~repos /atoms-element
git clone https://pyrossh.dev/repos/atoms-element.git
A simple web component library for defining your custom elements. It works on both client and server.
7dc696c6
—
Peter John 4 years ago
implement easy html parsing
- example/app-counter.js +1 -1
- example/index.js +1 -2
- example/server.js +1 -1
- element.d.ts → index.d.ts +41 -1
- element.js → index.js +281 -3
- element.test.js → index.test.js +76 -1
- package-lock.json +4 -5
- package.json +0 -3
- page.d.ts +0 -40
- page.js +0 -62
- page.test.js +0 -85
example/app-counter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement, html, object, number, string, unsafeHTML } from '../
|
|
1
|
+
import { createElement, html, object, number, string, unsafeHTML } from '../index.js';
|
|
2
2
|
|
|
3
3
|
const name = () => 'app-counter';
|
|
4
4
|
|
example/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { html, css } from '../element.js';
|
|
2
|
-
import { createPage } from '../
|
|
1
|
+
import { createPage, html, css } from '../index.js';
|
|
3
2
|
import { pageStyles } from './styles.js';
|
|
4
3
|
import './app-counter.js';
|
|
5
4
|
|
example/server.js
CHANGED
|
@@ -9,7 +9,7 @@ const __dirname = dirname(__filename);
|
|
|
9
9
|
|
|
10
10
|
const port = process.argv[2] || 3000;
|
|
11
11
|
const srcMap = {
|
|
12
|
-
'/
|
|
12
|
+
'/index.js': `${__dirname}/../index.js`,
|
|
13
13
|
'/lit-html.js': `${__dirname}/../lit-html.js`,
|
|
14
14
|
'/styles.js': `${__dirname}/styles.js`,
|
|
15
15
|
'/app-counter.js': `${__dirname}/app-counter.js`,
|
element.d.ts → index.d.ts
RENAMED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
export declare type Config = {
|
|
2
|
+
version: string
|
|
3
|
+
url: string
|
|
4
|
+
image: string
|
|
5
|
+
author: string
|
|
6
|
+
languages: Array<string>
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
keywords: string
|
|
1
|
-
|
|
10
|
+
categories: Array<any>
|
|
11
|
+
tags: Array<any>
|
|
12
|
+
strings: {[key: string]: any}
|
|
13
|
+
themes: {[key: string]: any}
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
export declare type Location = {
|
|
4
17
|
readonly ancestorOrigins: DOMStringList;
|
|
@@ -17,6 +30,9 @@ export declare type Location = {
|
|
|
17
30
|
toString: () => string;
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
export declare type Data = any;
|
|
34
|
+
export declare type Item = any;
|
|
35
|
+
|
|
20
36
|
export class AtomsElement {
|
|
21
37
|
static register(): () => void;
|
|
22
38
|
static observedAttributes: Array<string>;
|
|
@@ -37,3 +53,27 @@ export type CreateElementProps = {
|
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
export const createElement = (props: CreateElementProps) => CreateElementProps;
|
|
56
|
+
|
|
57
|
+
export type HandlerProps = {
|
|
58
|
+
config: Config;
|
|
59
|
+
data: Data;
|
|
60
|
+
item: Item;
|
|
61
|
+
}
|
|
62
|
+
export type Handler = (props: HandlerProps) => string;
|
|
63
|
+
|
|
64
|
+
export type CreatePageProps = {
|
|
65
|
+
route: Handler;
|
|
66
|
+
datapaths: Handler;
|
|
67
|
+
head: Handler;
|
|
68
|
+
body: Handler;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type PageRenderProps = {
|
|
72
|
+
config: Config;
|
|
73
|
+
data: Data;
|
|
74
|
+
item: Item;
|
|
75
|
+
headScript: string;
|
|
76
|
+
bodyScript: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const createPage = (props: CreatePageProps) => (props: PageRenderProps) => string;
|
element.js → index.js
RENAMED
|
@@ -375,7 +375,7 @@ const classLookup = {
|
|
|
375
375
|
export const getTWStyleSheet = (template) => {
|
|
376
376
|
const classList = template.strings
|
|
377
377
|
.reduce((acc, item) => {
|
|
378
|
-
const matches = item.match(/
|
|
378
|
+
const matches = item.match(/class=(?:["']\W+\s*(?:\w+)\()?["']([^'"]+)['"]/gim);
|
|
379
379
|
if (matches) {
|
|
380
380
|
matches.forEach((matched) => {
|
|
381
381
|
acc += matched.replace('class="', '').replace('"', '') + ' ';
|
|
@@ -417,6 +417,239 @@ const wrapAttribute = (attrName, suffix, text, v) => {
|
|
|
417
417
|
return buffer;
|
|
418
418
|
};
|
|
419
419
|
|
|
420
|
+
const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
|
|
421
|
+
const whitespaceRE = /^\s*$/;
|
|
422
|
+
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
|
|
423
|
+
const voidElements = {
|
|
424
|
+
area: true,
|
|
425
|
+
base: true,
|
|
426
|
+
br: true,
|
|
427
|
+
col: true,
|
|
428
|
+
embed: true,
|
|
429
|
+
hr: true,
|
|
430
|
+
img: true,
|
|
431
|
+
input: true,
|
|
432
|
+
link: true,
|
|
433
|
+
meta: true,
|
|
434
|
+
param: true,
|
|
435
|
+
source: true,
|
|
436
|
+
track: true,
|
|
437
|
+
wbr: true,
|
|
438
|
+
};
|
|
439
|
+
const empty = Object.create(null);
|
|
440
|
+
|
|
441
|
+
const parseTag = (tag) => {
|
|
442
|
+
const res = {
|
|
443
|
+
type: 'tag',
|
|
444
|
+
name: '',
|
|
445
|
+
voidElement: false,
|
|
446
|
+
attrs: {},
|
|
447
|
+
children: [],
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
|
|
451
|
+
if (tagMatch) {
|
|
452
|
+
res.name = tagMatch[1];
|
|
453
|
+
if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
|
|
454
|
+
res.voidElement = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// handle comment tag
|
|
458
|
+
if (res.name.startsWith('!--')) {
|
|
459
|
+
const endIndex = tag.indexOf('-->');
|
|
460
|
+
return {
|
|
461
|
+
type: 'comment',
|
|
462
|
+
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const reg = new RegExp(attrRE);
|
|
468
|
+
let result = null;
|
|
469
|
+
for (;;) {
|
|
470
|
+
result = reg.exec(tag);
|
|
471
|
+
|
|
472
|
+
if (result === null) {
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!result[0].trim()) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (result[1]) {
|
|
481
|
+
const attr = result[1].trim();
|
|
482
|
+
let arr = [attr, ''];
|
|
483
|
+
|
|
484
|
+
if (attr.indexOf('=') > -1) {
|
|
485
|
+
arr = attr.split('=');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
res.attrs[arr[0]] = arr[1];
|
|
489
|
+
reg.lastIndex--;
|
|
490
|
+
} else if (result[2]) {
|
|
491
|
+
res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return res;
|
|
496
|
+
};
|
|
497
|
+
const parseHtml = (html, options) => {
|
|
498
|
+
options || (options = {});
|
|
499
|
+
options.components || (options.components = empty);
|
|
500
|
+
const result = [];
|
|
501
|
+
const arr = [];
|
|
502
|
+
let current;
|
|
503
|
+
let level = -1;
|
|
504
|
+
let inComponent = false;
|
|
505
|
+
|
|
506
|
+
// handle text at top level
|
|
507
|
+
if (html.indexOf('<') !== 0) {
|
|
508
|
+
var end = html.indexOf('<');
|
|
509
|
+
result.push({
|
|
510
|
+
type: 'text',
|
|
511
|
+
content: end === -1 ? html : html.substring(0, end),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
html.replace(tagRE, function (tag, index) {
|
|
516
|
+
if (inComponent) {
|
|
517
|
+
if (tag !== '</' + current.name + '>') {
|
|
518
|
+
return;
|
|
519
|
+
} else {
|
|
520
|
+
inComponent = false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const isOpen = tag.charAt(1) !== '/';
|
|
524
|
+
const isComment = tag.startsWith('<!--');
|
|
525
|
+
const start = index + tag.length;
|
|
526
|
+
const nextChar = html.charAt(start);
|
|
527
|
+
let parent;
|
|
528
|
+
|
|
529
|
+
if (isComment) {
|
|
530
|
+
const comment = parseTag(tag);
|
|
531
|
+
|
|
532
|
+
// if we're at root, push new base node
|
|
533
|
+
if (level < 0) {
|
|
534
|
+
result.push(comment);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
parent = arr[level];
|
|
538
|
+
parent.children.push(comment);
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (isOpen) {
|
|
543
|
+
level++;
|
|
544
|
+
|
|
545
|
+
current = parseTag(tag);
|
|
546
|
+
if (current.type === 'tag' && options.components[current.name]) {
|
|
547
|
+
current.type = 'component';
|
|
548
|
+
inComponent = true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!current.voidElement && !inComponent && nextChar && nextChar !== '<') {
|
|
552
|
+
current.children.push({
|
|
553
|
+
type: 'text',
|
|
554
|
+
content: html.slice(start, html.indexOf('<', start)),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// if we're at root, push new base node
|
|
559
|
+
if (level === 0) {
|
|
560
|
+
result.push(current);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
parent = arr[level - 1];
|
|
564
|
+
|
|
565
|
+
if (parent) {
|
|
566
|
+
parent.children.push(current);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
arr[level] = current;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!isOpen || current.voidElement) {
|
|
573
|
+
if (level > -1 && (current.voidElement || current.name === tag.slice(2, -1))) {
|
|
574
|
+
level--;
|
|
575
|
+
// move current up a level to match the end tag
|
|
576
|
+
current = level === -1 ? result : arr[level];
|
|
577
|
+
}
|
|
578
|
+
if (!inComponent && nextChar !== '<' && nextChar) {
|
|
579
|
+
// trailing text node
|
|
580
|
+
// if we're at the root, push a base text node. otherwise add as
|
|
581
|
+
// a child to the current node.
|
|
582
|
+
parent = level === -1 ? result : arr[level].children;
|
|
583
|
+
|
|
584
|
+
// calculate correct end of the content slice in case there's
|
|
585
|
+
// no tag after the text node.
|
|
586
|
+
const end = html.indexOf('<', start);
|
|
587
|
+
let content = html.slice(start, end === -1 ? undefined : end);
|
|
588
|
+
// if a node is nothing but whitespace, collapse it as the spec states:
|
|
589
|
+
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
|
|
590
|
+
if (whitespaceRE.test(content)) {
|
|
591
|
+
content = ' ';
|
|
592
|
+
}
|
|
593
|
+
// don't add whitespace-only text nodes if they would be trailing text nodes
|
|
594
|
+
// or if they would be leading whitespace-only text nodes:
|
|
595
|
+
// * end > -1 indicates this is not a trailing text node
|
|
596
|
+
// * leading node is when level is -1 and parent has length 0
|
|
597
|
+
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
|
|
598
|
+
parent.push({
|
|
599
|
+
type: 'text',
|
|
600
|
+
content: content,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return result;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const stringifyAttrs = (attrs) => {
|
|
611
|
+
const buff = [];
|
|
612
|
+
for (let key in attrs) {
|
|
613
|
+
buff.push(key + '="' + attrs[key] + '"');
|
|
614
|
+
}
|
|
615
|
+
if (!buff.length) {
|
|
616
|
+
return '';
|
|
617
|
+
}
|
|
618
|
+
return ' ' + buff.join(' ');
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const stringifyHtml = (buff, doc) => {
|
|
622
|
+
switch (doc.type) {
|
|
623
|
+
case 'text':
|
|
624
|
+
return buff + doc.content;
|
|
625
|
+
case 'tag':
|
|
626
|
+
buff += '<' + doc.name + (doc.attrs ? stringifyAttrs(doc.attrs) : '') + (doc.voidElement ? '/>' : '>');
|
|
627
|
+
if (doc.voidElement) {
|
|
628
|
+
return buff;
|
|
629
|
+
}
|
|
630
|
+
return buff + doc.children.reduce(stringifyHtml, '') + '</' + doc.name + '>';
|
|
631
|
+
case 'comment':
|
|
632
|
+
buff += '<!--' + doc.comment + '-->';
|
|
633
|
+
return buff;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const hydrate = (node) => {
|
|
638
|
+
if (node.children) {
|
|
639
|
+
for (const child of node.children) {
|
|
640
|
+
const Clazz = AtomsElement.getElement(child.name);
|
|
641
|
+
if (Clazz) {
|
|
642
|
+
const instance = new Clazz(child.attrs);
|
|
643
|
+
const res = instance.renderTemplate();
|
|
644
|
+
child.children.push(...parseHtml(res));
|
|
645
|
+
}
|
|
646
|
+
if (child.children) {
|
|
647
|
+
hydrate(child);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
420
653
|
export const render = isBrowser
|
|
421
654
|
? litRender
|
|
422
655
|
: (template) => {
|
|
@@ -460,7 +693,14 @@ export const render = isBrowser
|
|
|
460
693
|
// console.log('value', value);
|
|
461
694
|
}
|
|
462
695
|
});
|
|
696
|
+
const nodes = parseHtml(js);
|
|
697
|
+
for (const node of nodes) {
|
|
698
|
+
hydrate(node);
|
|
699
|
+
}
|
|
700
|
+
const html = nodes.reduce((acc, node) => {
|
|
701
|
+
return acc + stringifyHtml('', node);
|
|
702
|
+
}, '');
|
|
463
|
-
return
|
|
703
|
+
return html;
|
|
464
704
|
};
|
|
465
705
|
|
|
466
706
|
const previousValues = new WeakMap();
|
|
@@ -705,7 +945,7 @@ export class AtomsElement extends BaseElement {
|
|
|
705
945
|
get attrs() {
|
|
706
946
|
return Object.keys(this.constructor.attrTypes).reduceRight((acc, key) => {
|
|
707
947
|
const attrType = this.constructor.attrTypes[key];
|
|
708
|
-
const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.ssrAttributes
|
|
948
|
+
const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.ssrAttributes[key.toLowerCase()];
|
|
709
949
|
const data = attrType.parse(newValue);
|
|
710
950
|
attrType.validate(`<${this.constructor.name}> ${key}`, data);
|
|
711
951
|
acc[key] = data;
|
|
@@ -791,3 +1031,41 @@ export const createElement = ({ name, attrTypes, stateTypes, computedTypes, rend
|
|
|
791
1031
|
Element.register();
|
|
792
1032
|
return { name, attrTypes, stateTypes, computedTypes, render };
|
|
793
1033
|
};
|
|
1034
|
+
|
|
1035
|
+
export const createPage = ({ route, datapaths, head, body }) => {
|
|
1036
|
+
return ({ config, data, item, headScript, bodyScript }) => {
|
|
1037
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
1038
|
+
const props = { config, data, item };
|
|
1039
|
+
const headHtml = render(head(props));
|
|
1040
|
+
const bodyTemplate = body(props);
|
|
1041
|
+
const bodyHtml = render(bodyTemplate);
|
|
1042
|
+
return `
|
|
1043
|
+
<!DOCTYPE html>
|
|
1044
|
+
<html lang="${config.lang}">
|
|
1045
|
+
<head>
|
|
1046
|
+
<meta charset="utf-8" />
|
|
1047
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
1048
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
1049
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
1050
|
+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
1051
|
+
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
1052
|
+
${headHtml}
|
|
1053
|
+
<style>
|
|
1054
|
+
${getTWStyleSheet(bodyTemplate)}
|
|
1055
|
+
</style>
|
|
1056
|
+
${headScript}
|
|
1057
|
+
</head>
|
|
1058
|
+
<body>
|
|
1059
|
+
${bodyHtml}
|
|
1060
|
+
<script>
|
|
1061
|
+
window.__DEV__ = ${!isProd};
|
|
1062
|
+
window.config = ${JSON.stringify(config)};
|
|
1063
|
+
window.data = ${JSON.stringify(data)};
|
|
1064
|
+
window.item = ${JSON.stringify(item)};
|
|
1065
|
+
</script>
|
|
1066
|
+
${bodyScript}
|
|
1067
|
+
</body>
|
|
1068
|
+
</html>
|
|
1069
|
+
`;
|
|
1070
|
+
};
|
|
1071
|
+
};
|
element.test.js → index.test.js
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test, jest } from '@jest/globals';
|
|
2
|
-
import { AtomsElement, html, render, number, boolean, string, array, object, unsafeHTML, css, classMap
|
|
2
|
+
import { AtomsElement, createElement, createPage, html, render, number, boolean, string, array, object, unsafeHTML, css, classMap } from './index.js';
|
|
3
3
|
|
|
4
4
|
global.__DEV = true;
|
|
5
5
|
|
|
@@ -389,3 +389,78 @@ test('createElement ', async () => {
|
|
|
389
389
|
const res = instance.renderTemplate();
|
|
390
390
|
expect(res).toEqual(` <div></div> `);
|
|
391
391
|
});
|
|
392
|
+
|
|
393
|
+
test('Page', () => {
|
|
394
|
+
const route = () => {
|
|
395
|
+
const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
|
|
396
|
+
return `${langPart}`;
|
|
397
|
+
};
|
|
398
|
+
const head = ({ config }) => {
|
|
399
|
+
return html`
|
|
400
|
+
<title>${config.title}</title>
|
|
401
|
+
<meta name="title" content=${config.title} />
|
|
402
|
+
<meta name="description" content=${config.title} />
|
|
403
|
+
`;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const body = ({ config }) => {
|
|
407
|
+
return html`
|
|
408
|
+
<div>
|
|
409
|
+
<app-header></app-header>
|
|
410
|
+
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
411
|
+
<h1 class="text-5xl">${config.title}</h1>
|
|
412
|
+
</main>
|
|
413
|
+
</div>
|
|
414
|
+
`;
|
|
415
|
+
};
|
|
416
|
+
const renderPage = createPage({
|
|
417
|
+
route,
|
|
418
|
+
head,
|
|
419
|
+
body,
|
|
420
|
+
});
|
|
421
|
+
const scripts = '<script type="module"><script>';
|
|
422
|
+
const res = renderPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
|
|
423
|
+
expect(res).toEqual(`
|
|
424
|
+
<!DOCTYPE html>
|
|
425
|
+
<html lang="en">
|
|
426
|
+
<head>
|
|
427
|
+
<meta charset="utf-8" />
|
|
428
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
429
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
430
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
431
|
+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
432
|
+
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
433
|
+
|
|
434
|
+
<title>123</title>
|
|
435
|
+
<meta name="title" content="123">
|
|
436
|
+
<meta name="description" content="123">
|
|
437
|
+
|
|
438
|
+
<style>
|
|
439
|
+
.div-1gao8uk {
|
|
440
|
+
color: red;
|
|
441
|
+
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
</style>
|
|
445
|
+
<script type="module"><script>
|
|
446
|
+
</head>
|
|
447
|
+
<body>
|
|
448
|
+
|
|
449
|
+
<div>
|
|
450
|
+
<app-header></app-header>
|
|
451
|
+
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
452
|
+
<h1 class="text-5xl">123</h1>
|
|
453
|
+
</main>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<script>
|
|
457
|
+
window.__DEV__ = true;
|
|
458
|
+
window.config = {"lang":"en","title":"123"};
|
|
459
|
+
window.data = undefined;
|
|
460
|
+
window.item = undefined;
|
|
461
|
+
</script>
|
|
462
|
+
<script type="module"><script>
|
|
463
|
+
</body>
|
|
464
|
+
</html>
|
|
465
|
+
`);
|
|
466
|
+
});
|
package-lock.json
CHANGED
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
"": {
|
|
8
8
|
"version": "2.3.0",
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"parse5": "^6.0.1"
|
|
12
|
-
},
|
|
13
10
|
"devDependencies": {
|
|
14
11
|
"jest": "^27.0.5"
|
|
15
12
|
},
|
|
@@ -3198,7 +3195,8 @@
|
|
|
3198
3195
|
"node_modules/parse5": {
|
|
3199
3196
|
"version": "6.0.1",
|
|
3200
3197
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
|
3201
|
-
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
|
3198
|
+
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
|
3199
|
+
"dev": true
|
|
3202
3200
|
},
|
|
3203
3201
|
"node_modules/path-exists": {
|
|
3204
3202
|
"version": "4.0.0",
|
|
@@ -6426,7 +6424,8 @@
|
|
|
6426
6424
|
"parse5": {
|
|
6427
6425
|
"version": "6.0.1",
|
|
6428
6426
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
|
6429
|
-
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
|
6427
|
+
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
|
6428
|
+
"dev": true
|
|
6430
6429
|
},
|
|
6431
6430
|
"path-exists": {
|
|
6432
6431
|
"version": "4.0.0",
|
package.json
CHANGED
|
@@ -41,8 +41,5 @@
|
|
|
41
41
|
"singleQuote": true,
|
|
42
42
|
"arrowParens": "always",
|
|
43
43
|
"trailingComma": "all"
|
|
44
|
-
},
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"parse5": "^6.0.1"
|
|
47
44
|
}
|
|
48
45
|
}
|
page.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export declare type Config = {
|
|
2
|
-
version: string
|
|
3
|
-
url: string
|
|
4
|
-
image: string
|
|
5
|
-
author: string
|
|
6
|
-
languages: Array<string>
|
|
7
|
-
title: string
|
|
8
|
-
description: string
|
|
9
|
-
keywords: string
|
|
10
|
-
categories: Array<any>
|
|
11
|
-
tags: Array<any>
|
|
12
|
-
strings: {[key: string]: any}
|
|
13
|
-
themes: {[key: string]: any}
|
|
14
|
-
}
|
|
15
|
-
export declare type Data = any;
|
|
16
|
-
export declare type Item = any;
|
|
17
|
-
|
|
18
|
-
export type find = (node: any) => void;
|
|
19
|
-
export type ssr = (template: any) => string;
|
|
20
|
-
|
|
21
|
-
export type Props = {
|
|
22
|
-
config: Config;
|
|
23
|
-
data: Data;
|
|
24
|
-
item: Item;
|
|
25
|
-
}
|
|
26
|
-
export type Handler = (props: Props) => string;
|
|
27
|
-
export type CreatePageProps = {
|
|
28
|
-
route: Handler;
|
|
29
|
-
datapaths: Handler;
|
|
30
|
-
head: Handler;
|
|
31
|
-
body: Handler;
|
|
32
|
-
}
|
|
33
|
-
export type PageRenderProps = {
|
|
34
|
-
config: Config;
|
|
35
|
-
data: Data;
|
|
36
|
-
item: Item;
|
|
37
|
-
headScript: string;
|
|
38
|
-
bodyScript: string;
|
|
39
|
-
}
|
|
40
|
-
export const createPage = (props: CreatePageProps) => (props: PageRenderProps) => string;
|
page.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import parse5 from 'parse5';
|
|
2
|
-
import { AtomsElement, render, getTWStyleSheet } from './element.js';
|
|
3
|
-
|
|
4
|
-
export const find = (node) => {
|
|
5
|
-
for (const child of node.childNodes) {
|
|
6
|
-
if (AtomsElement.getElement(child.tagName)) {
|
|
7
|
-
const Clazz = AtomsElement.getElement(child.tagName);
|
|
8
|
-
const instance = new Clazz(child.attrs);
|
|
9
|
-
const res = instance.renderTemplate();
|
|
10
|
-
const frag = parse5.parseFragment(res);
|
|
11
|
-
child.childNodes.push(...frag.childNodes);
|
|
12
|
-
}
|
|
13
|
-
if (child.childNodes) {
|
|
14
|
-
find(child);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const ssr = (template) => {
|
|
20
|
-
const text = render(template);
|
|
21
|
-
const h = parse5.parseFragment(text);
|
|
22
|
-
find(h);
|
|
23
|
-
return parse5.serialize(h);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const createPage = ({ route, datapaths, head, body, styles }) => {
|
|
27
|
-
return ({ config, data, item, headScript, bodyScript }) => {
|
|
28
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
29
|
-
const props = { config, data, item };
|
|
30
|
-
const headHtml = ssr(head(props));
|
|
31
|
-
const bodyTemplate = body(props);
|
|
32
|
-
const bodyHtml = ssr(bodyTemplate);
|
|
33
|
-
return `
|
|
34
|
-
<!DOCTYPE html>
|
|
35
|
-
<html lang="${config.lang}">
|
|
36
|
-
<head>
|
|
37
|
-
<meta charset="utf-8" />
|
|
38
|
-
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
39
|
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
40
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
41
|
-
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
42
|
-
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
43
|
-
${headHtml}
|
|
44
|
-
<style>
|
|
45
|
-
${getTWStyleSheet(bodyTemplate)}
|
|
46
|
-
</style>
|
|
47
|
-
${headScript}
|
|
48
|
-
</head>
|
|
49
|
-
<body>
|
|
50
|
-
${bodyHtml}
|
|
51
|
-
<script>
|
|
52
|
-
window.__DEV__ = ${!isProd};
|
|
53
|
-
window.config = ${JSON.stringify(config)};
|
|
54
|
-
window.data = ${JSON.stringify(data)};
|
|
55
|
-
window.item = ${JSON.stringify(item)};
|
|
56
|
-
</script>
|
|
57
|
-
${bodyScript}
|
|
58
|
-
</body>
|
|
59
|
-
</html>
|
|
60
|
-
`;
|
|
61
|
-
};
|
|
62
|
-
};
|
page.test.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { expect, test } from '@jest/globals';
|
|
2
|
-
import { html, css } from './element.js';
|
|
3
|
-
import { createPage } from './page.js';
|
|
4
|
-
|
|
5
|
-
test('Page', () => {
|
|
6
|
-
const route = () => {
|
|
7
|
-
const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
|
|
8
|
-
return `${langPart}`;
|
|
9
|
-
};
|
|
10
|
-
const styles = css({
|
|
11
|
-
div: {
|
|
12
|
-
color: 'red',
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
const head = ({ config }) => {
|
|
16
|
-
return html`
|
|
17
|
-
<title>${config.title}</title>
|
|
18
|
-
<meta name="title" content=${config.title} />
|
|
19
|
-
<meta name="description" content=${config.title} />
|
|
20
|
-
`;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const body = ({ config }) => {
|
|
24
|
-
return html`
|
|
25
|
-
<div>
|
|
26
|
-
<app-header></app-header>
|
|
27
|
-
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
28
|
-
<h1 class="text-5xl">${config.title}</h1>
|
|
29
|
-
</main>
|
|
30
|
-
</div>
|
|
31
|
-
`;
|
|
32
|
-
};
|
|
33
|
-
const renderPage = createPage({
|
|
34
|
-
route,
|
|
35
|
-
head,
|
|
36
|
-
body,
|
|
37
|
-
styles,
|
|
38
|
-
});
|
|
39
|
-
const scripts = '<script type="module"><script>';
|
|
40
|
-
const res = renderPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
|
|
41
|
-
expect(res).toEqual(`
|
|
42
|
-
<!DOCTYPE html>
|
|
43
|
-
<html lang="en">
|
|
44
|
-
<head>
|
|
45
|
-
<meta charset="utf-8" />
|
|
46
|
-
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
47
|
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
48
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
49
|
-
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
50
|
-
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
51
|
-
|
|
52
|
-
<title>123</title>
|
|
53
|
-
<meta name="title" content="123">
|
|
54
|
-
<meta name="description" content="123">
|
|
55
|
-
|
|
56
|
-
<style>
|
|
57
|
-
.div-1gao8uk {
|
|
58
|
-
color: red;
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
</style>
|
|
64
|
-
<script type="module"><script>
|
|
65
|
-
</head>
|
|
66
|
-
<body>
|
|
67
|
-
|
|
68
|
-
<div>
|
|
69
|
-
<app-header></app-header>
|
|
70
|
-
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
71
|
-
<h1 class="text-5xl">123</h1>
|
|
72
|
-
</main>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<script>
|
|
76
|
-
window.__DEV__ = true;
|
|
77
|
-
window.config = {"lang":"en","title":"123"};
|
|
78
|
-
window.data = undefined;
|
|
79
|
-
window.item = undefined;
|
|
80
|
-
</script>
|
|
81
|
-
<script type="module"><script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|
|
84
|
-
`);
|
|
85
|
-
});
|