boxwood 0.59.0 → 0.60.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/package.json +18 -17
- package/src/Importer.js +3 -3
- package/src/compilers/html/Transpiler.js +1 -13
- package/src/compilers/js/Bundler.js +60 -51
- package/src/lexers/html.js +49 -0
- package/src/{utilities/lexer.js → lexers/internal.js} +1 -1
- package/src/linters/html/bracket.js +36 -0
- package/src/linters/html/component.js +43 -0
- package/src/linters/html/import.js +35 -0
- package/src/linters/html/index.js +19 -0
- package/src/linters/html/tag.js +36 -0
- package/src/optimizers/html.js +15 -0
- package/src/render.js +4 -2
- package/src/transpilers/html/expression.js +1 -1
- package/src/transpilers/html/index.js +1 -5
- package/src/transpilers/html/node.js +26 -224
- package/src/transpilers/html/tags/any.js +53 -0
- package/src/transpilers/html/tags/comment.js +7 -0
- package/src/transpilers/html/tags/for.js +30 -0
- package/src/transpilers/html/tags/if.js +55 -0
- package/src/transpilers/html/tags/index.js +8 -1
- package/src/transpilers/html/tags/partial.js +2 -2
- package/src/transpilers/html/tags/text.js +6 -0
- package/src/transpilers/html/tags/try.js +30 -0
- package/src/transpilers/html/tags/unless.js +58 -0
- package/src/transpilers/html/utilities/path.js +9 -0
- package/src/utilities/optimize.js +1 -1
- package/src/utilities/string.js +1 -1
- package/src/Linter.js +0 -112
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boxwood",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.2",
|
|
4
4
|
"description": "Compile HTML templates into JS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "standard",
|
|
8
|
-
"test": "ava 'test/spec/**/*.spec.js' 'src/**/*.spec.js'",
|
|
8
|
+
"test": "ava 'test/spec/**/*.spec.js' 'src/**/*.spec.js' --no-worker-threads",
|
|
9
9
|
"coverage": "nyc npm test",
|
|
10
10
|
"benchmark": "ava test/benchmark.spec.js --verbose",
|
|
11
11
|
"watch": "npm test -- --watch",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">= 16.13.1"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
@@ -47,41 +47,42 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/buxlabs/boxwood#readme",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@rollup/plugin-commonjs": "^21.0.
|
|
51
|
-
"@rollup/plugin-node-resolve": "^13.
|
|
52
|
-
"abstract-syntax-tree": "^2.20.
|
|
50
|
+
"@rollup/plugin-commonjs": "^21.0.1",
|
|
51
|
+
"@rollup/plugin-node-resolve": "^13.1.3",
|
|
52
|
+
"abstract-syntax-tree": "^2.20.5",
|
|
53
53
|
"ansi-colors": "^4.1.1",
|
|
54
|
-
"axios": "^0.
|
|
54
|
+
"axios": "^0.25.0",
|
|
55
55
|
"axios-extensions": "3.1.3",
|
|
56
56
|
"css-tree": "^1.1.3",
|
|
57
|
-
"csso": "^
|
|
58
|
-
"esbuild": "^0.
|
|
57
|
+
"csso": "^5.0.2",
|
|
58
|
+
"esbuild": "^0.14.11",
|
|
59
59
|
"himalaya": "1.1.0",
|
|
60
60
|
"himalaya-walk": "1.0.0",
|
|
61
|
-
"html-lexer": "0.4.
|
|
61
|
+
"html-lexer": "^0.4.1",
|
|
62
|
+
"html-minifier": "4.0.0",
|
|
62
63
|
"memoizee": "0.4.15",
|
|
63
64
|
"negate-sentence": "0.2.0",
|
|
64
65
|
"path-to-regexp": "6.2.0",
|
|
65
66
|
"pure-conditions": "1.2.1",
|
|
66
|
-
"pure-utilities": "^1.2.
|
|
67
|
-
"rollup": "^2.
|
|
67
|
+
"pure-utilities": "^1.2.4",
|
|
68
|
+
"rollup": "^2.64.0",
|
|
68
69
|
"rollup-plugin-includepaths": "0.2.4",
|
|
69
70
|
"string-hash": "1.1.3",
|
|
70
71
|
"yaml": "^1.10.2"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
|
-
"ava": "
|
|
74
|
+
"ava": "^4.0.1",
|
|
74
75
|
"benchmark": "2.1.4",
|
|
75
76
|
"browser-env": "3.3.0",
|
|
76
|
-
"express": "4.17.
|
|
77
|
+
"express": "^4.17.2",
|
|
77
78
|
"handlebars": "^4.7.7",
|
|
78
79
|
"lodash.template": "4.5.0",
|
|
79
80
|
"mustache": "^4.2.0",
|
|
80
81
|
"nyc": "15.1.0",
|
|
81
|
-
"puppeteer": "^
|
|
82
|
+
"puppeteer": "^13.1.1",
|
|
82
83
|
"standard": "^16.0.4",
|
|
83
|
-
"typescript": "^4.4
|
|
84
|
-
"underscore": "^1.13.
|
|
84
|
+
"typescript": "^4.5.4",
|
|
85
|
+
"underscore": "^1.13.2"
|
|
85
86
|
},
|
|
86
87
|
"standard": {
|
|
87
88
|
"ignore": [
|
package/src/Importer.js
CHANGED
|
@@ -4,7 +4,7 @@ const { join, dirname } = require('path')
|
|
|
4
4
|
const { readFile, readFileWithCache, resolveAlias } = require('./utilities/files')
|
|
5
5
|
const { flatten } = require('pure-utilities/collection')
|
|
6
6
|
const Transpiler = require('./compilers/html/Transpiler')
|
|
7
|
-
const
|
|
7
|
+
const { lint } = require('./linters/html')
|
|
8
8
|
const request = require('./utilities/request')
|
|
9
9
|
const { getFullRemoteUrl, isRemotePath } = require('./utilities/url')
|
|
10
10
|
const { mergeAssets } = require('./utilities/assets')
|
|
@@ -13,7 +13,6 @@ const { getComponentNames } = require('./utilities/attributes')
|
|
|
13
13
|
const { getAssetPaths, getImportNodes } = require('./utilities/node')
|
|
14
14
|
const { parse } = require('./utilities/html')
|
|
15
15
|
const transpiler = new Transpiler()
|
|
16
|
-
const linter = new Linter()
|
|
17
16
|
|
|
18
17
|
let id = 1
|
|
19
18
|
|
|
@@ -111,7 +110,8 @@ async function recursiveImport (tree, source, path, options, depth, remote, url)
|
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
const imports = getImportNodes(tree, options)
|
|
114
|
-
const
|
|
113
|
+
const isHtmlPath = path === '.' || path.endsWith('.html')
|
|
114
|
+
const warnings = isHtmlPath ? lint(source, imports.map(({ node }) => node), options) : []
|
|
115
115
|
const assets = await Promise.all(imports.map(({ node, kind }) => fetch(node, kind, path, remote, url, options)))
|
|
116
116
|
const current = flatten(assets)
|
|
117
117
|
const nested = await Promise.all(current.filter(element => element.tree).map(async element => {
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const Lexer = require('html-lexer')
|
|
4
3
|
const { isCurlyTag, getTagValue } = require('../../utilities/string')
|
|
5
4
|
const { SPECIAL_TAGS } = require('../../utilities/enum')
|
|
6
|
-
|
|
7
|
-
const tokenize = (source) => {
|
|
8
|
-
const tokens = []
|
|
9
|
-
const delegate = {
|
|
10
|
-
write: (token) => tokens.push(token),
|
|
11
|
-
end: () => null
|
|
12
|
-
}
|
|
13
|
-
const lexer = new Lexer(delegate)
|
|
14
|
-
lexer.write(source)
|
|
15
|
-
lexer.end()
|
|
16
|
-
return tokens
|
|
17
|
-
}
|
|
5
|
+
const tokenize = require('../../lexers/html')
|
|
18
6
|
|
|
19
7
|
const transform = (tokens) => {
|
|
20
8
|
const output = []
|
|
@@ -3,63 +3,51 @@ const { join } = require('path')
|
|
|
3
3
|
const ESBundler = require('../../Bundler')
|
|
4
4
|
const { OBJECT_VARIABLE } = require('../../utilities/enum')
|
|
5
5
|
|
|
6
|
+
const { CallExpression, ExpressionStatement, FunctionExpression, Identifier, ImportDeclaration, ImportSpecifier, Literal } = AbstractSyntaxTree
|
|
7
|
+
|
|
8
|
+
function getRenderImportSpecifier () {
|
|
9
|
+
const identifier = new Identifier('render')
|
|
10
|
+
return new ImportSpecifier({
|
|
11
|
+
local: identifier,
|
|
12
|
+
imported: identifier
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getRenderCallExpression (node) {
|
|
17
|
+
return new CallExpression({
|
|
18
|
+
callee: new Identifier('render'),
|
|
19
|
+
arguments: [
|
|
20
|
+
new CallExpression({
|
|
21
|
+
callee: new FunctionExpression(node.declaration),
|
|
22
|
+
arguments: [new Identifier(OBJECT_VARIABLE)]
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBoxwoodImportDeclaration (node) {
|
|
29
|
+
return AbstractSyntaxTree.match(node, 'ImportDeclaration[source.value="boxwood"]')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getBoxwoodImportDeclaration () {
|
|
33
|
+
return new ImportDeclaration({
|
|
34
|
+
specifiers: [getRenderImportSpecifier()],
|
|
35
|
+
source: new Literal('boxwood')
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isFunctionExportDeclaration (node) {
|
|
40
|
+
return AbstractSyntaxTree.match(node, 'ExportDefaultDeclaration[declaration.type="FunctionDeclaration"]')
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
class Bundler {
|
|
7
44
|
constructor (options) {
|
|
8
45
|
this.options = options
|
|
9
46
|
}
|
|
10
47
|
|
|
11
48
|
async bundle (input) {
|
|
12
|
-
const tree =
|
|
13
|
-
|
|
14
|
-
tree.replace(node => {
|
|
15
|
-
if (node.type === 'ImportDeclaration' && node.source.value === 'boxwood') {
|
|
16
|
-
scoped = true
|
|
17
|
-
node.specifiers.push({
|
|
18
|
-
type: 'ImportSpecifier',
|
|
19
|
-
local: { type: 'Identifier', name: 'render' },
|
|
20
|
-
imported: { type: 'Identifier', name: 'render' }
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
if (!scoped) {
|
|
25
|
-
scoped = true
|
|
26
|
-
tree.prepend({
|
|
27
|
-
type: 'ImportDeclaration',
|
|
28
|
-
specifiers: [
|
|
29
|
-
{
|
|
30
|
-
type: 'ImportSpecifier',
|
|
31
|
-
local: { type: 'Identifier', name: 'render' },
|
|
32
|
-
imported: { type: 'Identifier', name: 'render' }
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
source: {
|
|
36
|
-
type: 'Literal',
|
|
37
|
-
value: 'boxwood'
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
tree.replace(node => {
|
|
42
|
-
if (scoped && node.type === 'ExportDefaultDeclaration' && node.declaration.type === 'FunctionDeclaration') {
|
|
43
|
-
return {
|
|
44
|
-
type: 'ExpressionStatement',
|
|
45
|
-
expression: {
|
|
46
|
-
type: 'CallExpression',
|
|
47
|
-
callee: {
|
|
48
|
-
type: 'Identifier',
|
|
49
|
-
name: 'render'
|
|
50
|
-
},
|
|
51
|
-
arguments: [
|
|
52
|
-
{
|
|
53
|
-
type: 'CallExpression',
|
|
54
|
-
callee: { ...node.declaration, type: 'FunctionExpression' },
|
|
55
|
-
arguments: [{ type: 'Identifier', name: OBJECT_VARIABLE }]
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return node
|
|
62
|
-
})
|
|
49
|
+
const tree = this.parse(input)
|
|
50
|
+
|
|
63
51
|
const bundler = new ESBundler()
|
|
64
52
|
return await bundler.bundle(tree.source, {
|
|
65
53
|
platform: 'node',
|
|
@@ -70,6 +58,27 @@ class Bundler {
|
|
|
70
58
|
]
|
|
71
59
|
})
|
|
72
60
|
}
|
|
61
|
+
|
|
62
|
+
parse (input) {
|
|
63
|
+
const tree = new AbstractSyntaxTree(input)
|
|
64
|
+
let imported = false
|
|
65
|
+
tree.replace(node => {
|
|
66
|
+
if (isBoxwoodImportDeclaration(node)) {
|
|
67
|
+
imported = true
|
|
68
|
+
node.specifiers.push(getRenderImportSpecifier())
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
if (!imported) {
|
|
72
|
+
tree.prepend(getBoxwoodImportDeclaration())
|
|
73
|
+
}
|
|
74
|
+
tree.replace(node => {
|
|
75
|
+
if (isFunctionExportDeclaration(node)) {
|
|
76
|
+
return new ExpressionStatement({ expression: getRenderCallExpression(node) })
|
|
77
|
+
}
|
|
78
|
+
return node
|
|
79
|
+
})
|
|
80
|
+
return tree
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
module.exports = Bundler
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const Lexer = require('html-lexer')
|
|
2
|
+
|
|
3
|
+
const tokenize = (source) => {
|
|
4
|
+
const tokens = []
|
|
5
|
+
const delegate = {
|
|
6
|
+
write: (token) => tokens.push(token),
|
|
7
|
+
end: () => null
|
|
8
|
+
}
|
|
9
|
+
const lexer = new Lexer(delegate)
|
|
10
|
+
lexer.write(source)
|
|
11
|
+
lexer.end()
|
|
12
|
+
return reduce(normalize(tokens))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalize = (tokens) => {
|
|
16
|
+
for (let i = 0, ilen = tokens.length; i < ilen; i += 1) {
|
|
17
|
+
const current = tokens[i]
|
|
18
|
+
if (current[0] === 'endTagPrefix') {
|
|
19
|
+
const previous = tokens[i - 1]
|
|
20
|
+
const next = tokens[i + 1]
|
|
21
|
+
if (previous && next && previous[0] === 'rawtext' && next[0] === 'rawtext') {
|
|
22
|
+
current[0] = 'rawtext'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return tokens
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const reduce = (tokens) => {
|
|
30
|
+
const newTokens = []
|
|
31
|
+
for (let i = 0, ilen = tokens.length; i < ilen; i += 1) {
|
|
32
|
+
const current = tokens[i]
|
|
33
|
+
if (current[0] === 'rawtext') {
|
|
34
|
+
let text = current[1]
|
|
35
|
+
let next = tokens[i + 1]
|
|
36
|
+
while (next && next[0] === 'rawtext') {
|
|
37
|
+
i++
|
|
38
|
+
text += next[1]
|
|
39
|
+
next = tokens[i + 1]
|
|
40
|
+
}
|
|
41
|
+
newTokens.push(['rawtext', text])
|
|
42
|
+
} else {
|
|
43
|
+
newTokens.push(tokens[i])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return newTokens
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = tokenize
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const tokenize = require('../../lexers/html')
|
|
2
|
+
|
|
3
|
+
const OPENING_ANGLE_BRACKET = '<'
|
|
4
|
+
const CLOSING_ANGLE_BRACKET = '>'
|
|
5
|
+
|
|
6
|
+
function isOpeningBracket (character) {
|
|
7
|
+
return character === OPENING_ANGLE_BRACKET
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isClosingBracket (character) {
|
|
11
|
+
return character === CLOSING_ANGLE_BRACKET
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function verifyBrackets (source) {
|
|
15
|
+
const tokens = tokenize(source).filter(token => token[0] !== 'rawtext')
|
|
16
|
+
const text = tokens.map(token => token[1]).join('')
|
|
17
|
+
|
|
18
|
+
const errors = []
|
|
19
|
+
let bracket = null
|
|
20
|
+
for (const character of text) {
|
|
21
|
+
if (isOpeningBracket(character)) {
|
|
22
|
+
if (isOpeningBracket(bracket)) {
|
|
23
|
+
errors.push({ type: 'CLOSING_ANGLE_BRACKET_MISSING', message: 'closing angle bracket is missing' })
|
|
24
|
+
}
|
|
25
|
+
bracket = OPENING_ANGLE_BRACKET
|
|
26
|
+
} else if (isClosingBracket(character)) {
|
|
27
|
+
if (!isOpeningBracket(bracket)) {
|
|
28
|
+
errors.push({ type: 'OPENING_ANGLE_BRACKET_MISSING', message: 'opening angle bracket is missing' })
|
|
29
|
+
}
|
|
30
|
+
bracket = CLOSING_ANGLE_BRACKET
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return errors
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { verifyBrackets }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const walk = require('himalaya-walk')
|
|
4
|
+
const { unique } = require('pure-utilities/array')
|
|
5
|
+
const { isImportTag } = require('../../utilities/string')
|
|
6
|
+
const { getComponentNames } = require('../../utilities/attributes')
|
|
7
|
+
|
|
8
|
+
function isStyleImport (node) {
|
|
9
|
+
const from = node.attributes.find(attr => attr.key === 'from')
|
|
10
|
+
if (from && from.value.endsWith('.css')) {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function analyze (tree) {
|
|
17
|
+
const components = []
|
|
18
|
+
walk(tree, node => {
|
|
19
|
+
if (isImportTag(node.tagName) && !isStyleImport(node)) {
|
|
20
|
+
const names = getComponentNames(node.attributes)
|
|
21
|
+
names.forEach(name => components.push(name))
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
return { components: unique(components) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function verifyComponents (tree) {
|
|
28
|
+
const warnings = []
|
|
29
|
+
const { components } = analyze(tree)
|
|
30
|
+
walk(tree, node => {
|
|
31
|
+
const index = components.indexOf(node.tagName)
|
|
32
|
+
if (index !== -1) {
|
|
33
|
+
components.splice(index, 1)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
// TODO: unify warnings.
|
|
37
|
+
components.forEach(component => {
|
|
38
|
+
warnings.push({ type: 'UNUSED_COMPONENT', message: `${component} component is unused` })
|
|
39
|
+
})
|
|
40
|
+
return warnings
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { verifyComponents, analyze }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { duplicates } = require('pure-utilities/array')
|
|
2
|
+
const { getAssetPaths, isImageNode } = require('../../utilities/node')
|
|
3
|
+
const { isImportTag } = require('../../utilities/string')
|
|
4
|
+
const { getComponentNames } = require('../../utilities/attributes')
|
|
5
|
+
|
|
6
|
+
function verifyImports (imports, options) {
|
|
7
|
+
const warnings = []
|
|
8
|
+
const allNames = []
|
|
9
|
+
let allPaths = []
|
|
10
|
+
imports.forEach(node => {
|
|
11
|
+
if (isImportTag(node.tagName)) {
|
|
12
|
+
const names = getComponentNames(node.attributes)
|
|
13
|
+
names.forEach(name => {
|
|
14
|
+
if (allNames.includes(name)) {
|
|
15
|
+
warnings.push({ message: `Component name duplicate: ${name}`, type: 'COMPONENT_NAME_DUPLICATE' })
|
|
16
|
+
} else {
|
|
17
|
+
allNames.push(name)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
imports.forEach(node => {
|
|
23
|
+
let assetPaths = getAssetPaths(node)
|
|
24
|
+
if (isImageNode(node, options)) {
|
|
25
|
+
assetPaths = assetPaths.filter(item => !assetPaths.includes(item))
|
|
26
|
+
}
|
|
27
|
+
allPaths = allPaths.concat(assetPaths)
|
|
28
|
+
})
|
|
29
|
+
duplicates(allPaths).forEach(duplicate => {
|
|
30
|
+
warnings.push({ message: `Component path duplicate: ${duplicate}`, type: 'COMPONENT_PATH_DUPLICATE' })
|
|
31
|
+
})
|
|
32
|
+
return warnings
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { verifyImports }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parse } = require('../../utilities/html')
|
|
4
|
+
const { verifyBrackets } = require('./bracket')
|
|
5
|
+
const { verifyTags } = require('./tag')
|
|
6
|
+
const { verifyComponents } = require('./component')
|
|
7
|
+
const { verifyImports } = require('./import')
|
|
8
|
+
|
|
9
|
+
function lint (source, imports = [], options = {}) {
|
|
10
|
+
const tree = parse(source)
|
|
11
|
+
return [
|
|
12
|
+
...verifyBrackets(source),
|
|
13
|
+
...verifyTags(tree),
|
|
14
|
+
...verifyComponents(tree),
|
|
15
|
+
...verifyImports(imports, options)
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { lint }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const walk = require('himalaya-walk')
|
|
2
|
+
const { analyze } = require('./component')
|
|
3
|
+
|
|
4
|
+
const ANCHOR_TAG = 'a'
|
|
5
|
+
const IMAGE_TAG = 'img'
|
|
6
|
+
|
|
7
|
+
function isExternalUrl (url) {
|
|
8
|
+
if (!url) { return false }
|
|
9
|
+
return url.startsWith('http://') || url.startsWith('https://')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function verifyTags (tree) {
|
|
13
|
+
const warnings = []
|
|
14
|
+
const { components } = analyze(tree)
|
|
15
|
+
walk(tree, node => {
|
|
16
|
+
if (node.tagName === ANCHOR_TAG && !components.includes(ANCHOR_TAG)) {
|
|
17
|
+
const href = node.attributes.find(attribute => attribute.key === 'href')
|
|
18
|
+
if (href && isExternalUrl(href.value)) {
|
|
19
|
+
const rel = node.attributes.find(attribute => attribute.key === 'rel')
|
|
20
|
+
if (!rel) {
|
|
21
|
+
warnings.push({ message: `${ANCHOR_TAG} tag with external href should have a rel attribute (e.g. rel="noopener")`, type: 'REL_ATTRIBUTE_MISSING' })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (node.tagName === IMAGE_TAG && !components.includes(IMAGE_TAG)) {
|
|
27
|
+
const alt = node.attributes.find(attribute => attribute.key === 'alt' || attribute.key.startsWith('alt|'))
|
|
28
|
+
if (!alt) {
|
|
29
|
+
warnings.push({ message: `${IMAGE_TAG} tag should have an alt attribute`, type: 'ALT_ATTRIBUTE_MISSING' })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
return warnings
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { verifyTags }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { minify } = require('html-minifier')
|
|
2
|
+
|
|
3
|
+
function optimize (html) {
|
|
4
|
+
return minify(html.trim(), {
|
|
5
|
+
collapseWhitespace: true,
|
|
6
|
+
conservativeCollapse: true,
|
|
7
|
+
removeComments: true,
|
|
8
|
+
sortAttributes: true,
|
|
9
|
+
sortClassName: true,
|
|
10
|
+
minifyCSS: true,
|
|
11
|
+
minifyJS: true
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { optimize }
|
package/src/render.js
CHANGED
|
@@ -4,6 +4,7 @@ const { readFile } = require('fs')
|
|
|
4
4
|
const { dirname } = require('path')
|
|
5
5
|
const { promisify } = require('util')
|
|
6
6
|
const { print } = require('./utilities/log')
|
|
7
|
+
const { optimize } = require('./optimizers/html')
|
|
7
8
|
|
|
8
9
|
const read = promisify(readFile)
|
|
9
10
|
|
|
@@ -32,8 +33,9 @@ function createRender ({
|
|
|
32
33
|
const template = await compileFile(path)
|
|
33
34
|
const params = typeof globals === 'function' ? globals(path, options) : globals
|
|
34
35
|
const html = template({ ...params, ...options }, escape)
|
|
35
|
-
|
|
36
|
-
return
|
|
36
|
+
const optimizedHtml = optimize(html)
|
|
37
|
+
if (callback) return callback(null, optimizedHtml)
|
|
38
|
+
return optimizedHtml
|
|
37
39
|
} catch (exception) {
|
|
38
40
|
if (callback) return callback(exception)
|
|
39
41
|
return exception.message
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
2
|
const { unique } = require('pure-utilities/array')
|
|
3
|
-
const lexer = require('../../
|
|
3
|
+
const lexer = require('../../lexers/internal')
|
|
4
4
|
const { BUILT_IN_VARIABLES } = require('../../utilities/enum')
|
|
5
5
|
|
|
6
6
|
const { ArrayExpression, CallExpression, Identifier, Literal, ObjectPattern, Property, toBinaryExpression } = AbstractSyntaxTree
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
4
|
-
const { camelize } = require('pure-utilities/string')
|
|
5
4
|
const { deduceParams } = require('./expression')
|
|
6
5
|
const BoxModelPlugin = require('../../plugins/BoxModelPlugin')
|
|
7
6
|
const CurlyStylesPlugin = require('../../plugins/CurlyStylesPlugin')
|
|
@@ -10,6 +9,7 @@ const { findAttributeByKey } = require('../../utilities/attributes')
|
|
|
10
9
|
const { transpileNode } = require('./node')
|
|
11
10
|
// TODO: initial transpilation, move to a separate dir? or inline here after removing the outdated compiler
|
|
12
11
|
const Transpiler = require('../../compilers/html/Transpiler')
|
|
12
|
+
const { pathToIdentifier } = require('./utilities/path')
|
|
13
13
|
|
|
14
14
|
const {
|
|
15
15
|
ArrayExpression,
|
|
@@ -20,10 +20,6 @@ const {
|
|
|
20
20
|
Literal
|
|
21
21
|
} = AbstractSyntaxTree
|
|
22
22
|
|
|
23
|
-
function pathToIdentifier (path) {
|
|
24
|
-
return `__${camelize(path).replace('.', 'Dot')}__`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
const program = (body) => {
|
|
28
24
|
const params = deduceParams(body)
|
|
29
25
|
return AbstractSyntaxTree.program(
|
|
@@ -1,244 +1,46 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
4
|
-
const { transpileExpression } = require('./expression')
|
|
5
4
|
const tags = require('./tags')
|
|
6
5
|
|
|
7
|
-
const {
|
|
8
|
-
ArrayExpression,
|
|
9
|
-
BlockStatement,
|
|
10
|
-
CallExpression,
|
|
11
|
-
Identifier,
|
|
12
|
-
IfStatement,
|
|
13
|
-
Literal,
|
|
14
|
-
ObjectExpression,
|
|
15
|
-
Property,
|
|
16
|
-
ReturnStatement,
|
|
17
|
-
TryStatement,
|
|
18
|
-
CatchClause,
|
|
19
|
-
UnaryExpression
|
|
20
|
-
} = AbstractSyntaxTree
|
|
21
|
-
|
|
22
|
-
let FOR_LOOP_INDEX = 0
|
|
23
|
-
|
|
24
|
-
function mapForStatement (htmlNode, parent, index) {
|
|
25
|
-
FOR_LOOP_INDEX++
|
|
26
|
-
const [node] = AbstractSyntaxTree.template(`
|
|
27
|
-
(function () {
|
|
28
|
-
var __output__ = [];
|
|
29
|
-
for (var <%= index %> = 0, <%= length %> = <%= array %>.length; <%= index %> < <%= length %>; <%= index %>++) {
|
|
30
|
-
var <%= item %> = <%= array %>[<%= index %>];
|
|
31
|
-
__output__.push(%= children %);
|
|
32
|
-
}
|
|
33
|
-
return __output__;
|
|
34
|
-
})();
|
|
35
|
-
`, {
|
|
36
|
-
index: new Identifier(`__i${FOR_LOOP_INDEX}__`),
|
|
37
|
-
length: new Identifier(`__ilen${FOR_LOOP_INDEX}__`),
|
|
38
|
-
item: new Identifier(htmlNode.attributes[0].key),
|
|
39
|
-
// TODO we should not mark params which were created on the fly, e.g. for nested loops
|
|
40
|
-
array: new Identifier({ name: htmlNode.attributes[2].key, parameter: true }),
|
|
41
|
-
children: htmlNode.children.map((child, index) =>
|
|
42
|
-
transpileNode({ node: child, parent: htmlNode, index: index })
|
|
43
|
-
)
|
|
44
|
-
})
|
|
45
|
-
return node.expression
|
|
46
|
-
}
|
|
47
|
-
|
|
48
6
|
function transpileNode ({ node: htmlNode, parent, index }) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
properties: attributes.map(attribute => {
|
|
57
|
-
return new Property({
|
|
58
|
-
key: new Identifier(attribute.key),
|
|
59
|
-
value: getAttributeValue(attribute.value),
|
|
60
|
-
kind: 'init',
|
|
61
|
-
computed: false,
|
|
62
|
-
method: false,
|
|
63
|
-
shorthand: false
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
: new ObjectExpression({ properties: [] })
|
|
68
|
-
}
|
|
69
|
-
function mapChildren (children) {
|
|
70
|
-
return children.length > 0 && new ArrayExpression({
|
|
71
|
-
elements: children.map((childNode, index) => {
|
|
72
|
-
return transpileNode({ node: childNode, parent: children, index })
|
|
73
|
-
}).filter(Boolean)
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
function mapIfStatement (htmlNode, parent, index) {
|
|
77
|
-
function mapAttributesToTest ({ attributes }) {
|
|
78
|
-
if (attributes.length === 1) {
|
|
79
|
-
if (attributes[0].key === 'true' || attributes[0].key === '{true}') {
|
|
80
|
-
return new Literal(true)
|
|
81
|
-
} else if (attributes[0].key === 'false' || attributes[0].key === '{false}') {
|
|
82
|
-
return new Literal(false)
|
|
83
|
-
} else {
|
|
84
|
-
return new Identifier({ name: attributes[0].key, parameter: true })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
throw new Error('Unsupported length of attributes (if)')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function mapCurrentNodeToConsequent (htmlNode) {
|
|
91
|
-
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
92
|
-
const argument = body.pop()
|
|
93
|
-
body.push(new ReturnStatement({ argument }))
|
|
94
|
-
return new BlockStatement({ body })
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mapNextNodeToAlternate (nextNode) {
|
|
98
|
-
if (nextNode && nextNode.tagName === 'else') {
|
|
99
|
-
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: nextNode.children, index })).filter(Boolean)
|
|
100
|
-
const argument = body.pop()
|
|
101
|
-
body.push(new ReturnStatement({ argument }))
|
|
102
|
-
return new BlockStatement({ body })
|
|
103
|
-
} else if (nextNode && nextNode.tagName === 'elseif') {
|
|
104
|
-
return mapIfStatement(nextNode, parent, index + 1)
|
|
105
|
-
}
|
|
106
|
-
return new BlockStatement({
|
|
107
|
-
body: [
|
|
108
|
-
new ReturnStatement({
|
|
109
|
-
argument: new Literal('')
|
|
110
|
-
})
|
|
111
|
-
]
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return new IfStatement({
|
|
116
|
-
test: mapAttributesToTest(htmlNode),
|
|
117
|
-
consequent: mapCurrentNodeToConsequent(htmlNode),
|
|
118
|
-
alternate: mapNextNodeToAlternate(parent[index + 1])
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function mapUnlessStatement (htmlNode, parent, index) {
|
|
123
|
-
function mapAttributesToTest ({ attributes }) {
|
|
124
|
-
if (attributes.length === 1) {
|
|
125
|
-
if (attributes[0].key === 'true' || attributes[0].key === '{true}') {
|
|
126
|
-
return new Literal(true)
|
|
127
|
-
} else if (attributes[0].key === 'false' || attributes[0].key === '{false}') {
|
|
128
|
-
return new Literal(false)
|
|
129
|
-
} else {
|
|
130
|
-
return new Identifier({ name: attributes[0].key, parameter: true })
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
throw new Error('Unsupported length of attributes (unless)')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function mapCurrentNodeToConsequent (htmlNode) {
|
|
137
|
-
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
138
|
-
const argument = body.pop()
|
|
139
|
-
body.push(new ReturnStatement({ argument }))
|
|
140
|
-
return new BlockStatement({ body })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function mapNextNodeToAlternate (nextNode) {
|
|
144
|
-
if (nextNode && nextNode.tagName === 'else') {
|
|
145
|
-
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: nextNode.children, index })).filter(Boolean)
|
|
146
|
-
const argument = body.pop()
|
|
147
|
-
body.push(new ReturnStatement({ argument }))
|
|
148
|
-
return new BlockStatement({ body })
|
|
149
|
-
} else if (nextNode && nextNode.tagName === 'elseunless') {
|
|
150
|
-
return mapUnlessStatement(nextNode, parent, index + 1)
|
|
151
|
-
} else if (nextNode && nextNode.tagName === 'elseif') {
|
|
152
|
-
return mapIfStatement(nextNode, parent, index + 1)
|
|
153
|
-
}
|
|
154
|
-
return new BlockStatement({
|
|
155
|
-
body: [
|
|
156
|
-
new ReturnStatement({
|
|
157
|
-
argument: new Literal('')
|
|
158
|
-
})
|
|
159
|
-
]
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return new IfStatement({
|
|
164
|
-
test: new UnaryExpression({
|
|
165
|
-
operator: '!',
|
|
166
|
-
argument: mapAttributesToTest(htmlNode),
|
|
167
|
-
prefix: true
|
|
168
|
-
}),
|
|
169
|
-
consequent: mapCurrentNodeToConsequent(htmlNode),
|
|
170
|
-
alternate: mapNextNodeToAlternate(parent[index + 1])
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function mapTryStatement (htmlNode, parent, index) {
|
|
175
|
-
function mapCurrentNodeToBlockStatement (htmlNode) {
|
|
176
|
-
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
177
|
-
const argument = body.pop()
|
|
178
|
-
body.push(new ReturnStatement({ argument }))
|
|
179
|
-
return new BlockStatement({ body })
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function mapNextNodeToCatchClause (nextNode) {
|
|
183
|
-
if (nextNode && nextNode.tagName === 'catch') {
|
|
184
|
-
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
185
|
-
const argument = body.pop()
|
|
186
|
-
body.push(new ReturnStatement({ argument }))
|
|
187
|
-
return new CatchClause({
|
|
188
|
-
body: new BlockStatement({ body })
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
return new TryStatement({
|
|
194
|
-
block: mapCurrentNodeToBlockStatement(htmlNode),
|
|
195
|
-
handler: mapNextNodeToCatchClause(parent[index + 1])
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (htmlNode.type === 'element' && htmlNode.tagName === 'if') {
|
|
200
|
-
const statement = mapIfStatement(htmlNode, parent, index)
|
|
7
|
+
const { type, tagName } = htmlNode
|
|
8
|
+
if (type === 'text') {
|
|
9
|
+
return tags.text(htmlNode)
|
|
10
|
+
} else if (type === 'comment') {
|
|
11
|
+
return tags.comment()
|
|
12
|
+
} else if (tagName === 'if') {
|
|
13
|
+
const statement = tags.if(htmlNode, parent, index, transpileNode)
|
|
201
14
|
const { expression } = AbstractSyntaxTree.iife(statement)
|
|
202
15
|
return expression
|
|
203
|
-
} else if (
|
|
16
|
+
} else if (tagName === 'else') {
|
|
204
17
|
return null
|
|
205
|
-
} else if (
|
|
18
|
+
} else if (tagName === 'elseif') {
|
|
206
19
|
return null
|
|
207
|
-
} else if (
|
|
208
|
-
const statement =
|
|
20
|
+
} else if (tagName === 'unless') {
|
|
21
|
+
const statement = tags.unless(htmlNode, parent, index, transpileNode)
|
|
209
22
|
const { expression } = AbstractSyntaxTree.iife(statement)
|
|
210
23
|
return expression
|
|
211
|
-
} else if (
|
|
24
|
+
} else if (tagName === 'elseunless') {
|
|
212
25
|
return null
|
|
213
|
-
} else if (
|
|
214
|
-
const statement =
|
|
26
|
+
} else if (tagName === 'try') {
|
|
27
|
+
const statement = tags.try(htmlNode, parent, index, transpileNode)
|
|
215
28
|
const { expression } = AbstractSyntaxTree.iife(statement)
|
|
216
29
|
return expression
|
|
217
|
-
} else if (
|
|
30
|
+
} else if (tagName === 'catch') {
|
|
218
31
|
return null
|
|
219
|
-
} else if (
|
|
220
|
-
return
|
|
221
|
-
} else if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
arguments: [
|
|
230
|
-
new Literal(tagName),
|
|
231
|
-
mapAttributes(attributes),
|
|
232
|
-
mapChildren(children)
|
|
233
|
-
].filter(Boolean)
|
|
234
|
-
})
|
|
235
|
-
return node
|
|
236
|
-
} else if (htmlNode.type === 'text') {
|
|
237
|
-
const { content } = htmlNode
|
|
238
|
-
return transpileExpression(content)
|
|
239
|
-
} else if (htmlNode.type === 'comment') {
|
|
240
|
-
return new Literal('')
|
|
32
|
+
} else if (tagName === 'for') {
|
|
33
|
+
return tags.for(htmlNode, parent, index, transpileNode)
|
|
34
|
+
} else if (tagName === 'import') {
|
|
35
|
+
return tags.import(htmlNode)
|
|
36
|
+
} else if (tagName === '!doctype') {
|
|
37
|
+
return tags.doctype()
|
|
38
|
+
} else if (tagName === 'partial') {
|
|
39
|
+
return tags.partial(htmlNode)
|
|
40
|
+
} else if (tagName === 'slot') {
|
|
41
|
+
return tags.slot(htmlNode)
|
|
241
42
|
}
|
|
43
|
+
return tags.any(htmlNode, transpileNode)
|
|
242
44
|
}
|
|
243
45
|
|
|
244
46
|
module.exports = { transpileNode }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
+
const { transpileExpression } = require('../expression')
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
ArrayExpression,
|
|
6
|
+
CallExpression,
|
|
7
|
+
Identifier,
|
|
8
|
+
Literal,
|
|
9
|
+
ObjectExpression,
|
|
10
|
+
Property
|
|
11
|
+
} = AbstractSyntaxTree
|
|
12
|
+
|
|
13
|
+
function mapAttributes (attributes) {
|
|
14
|
+
function getAttributeValue (value) {
|
|
15
|
+
if (value === null) { return new Literal(true) }
|
|
16
|
+
return transpileExpression(value, false)
|
|
17
|
+
}
|
|
18
|
+
return attributes.length > 0
|
|
19
|
+
? new ObjectExpression({
|
|
20
|
+
properties: attributes.map(attribute => {
|
|
21
|
+
return new Property({
|
|
22
|
+
key: new Identifier(attribute.key),
|
|
23
|
+
value: getAttributeValue(attribute.value),
|
|
24
|
+
kind: 'init',
|
|
25
|
+
computed: false,
|
|
26
|
+
method: false,
|
|
27
|
+
shorthand: false
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
: new ObjectExpression({ properties: [] })
|
|
32
|
+
}
|
|
33
|
+
function mapChildren (children, transpileNode) {
|
|
34
|
+
return children.length > 0 && new ArrayExpression({
|
|
35
|
+
elements: children.map((childNode, index) => {
|
|
36
|
+
return transpileNode({ node: childNode, parent: children, index })
|
|
37
|
+
}).filter(Boolean)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function any (htmlNode, transpileNode) {
|
|
42
|
+
const { tagName, attributes, children } = htmlNode
|
|
43
|
+
return new CallExpression({
|
|
44
|
+
callee: new Identifier('tag'),
|
|
45
|
+
arguments: [
|
|
46
|
+
new Literal(tagName),
|
|
47
|
+
mapAttributes(attributes),
|
|
48
|
+
mapChildren(children, transpileNode)
|
|
49
|
+
].filter(Boolean)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = any
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
+
const { Identifier } = AbstractSyntaxTree
|
|
3
|
+
|
|
4
|
+
let FOR_LOOP_INDEX = 0
|
|
5
|
+
|
|
6
|
+
function mapForStatement (htmlNode, parent, index, transpileNode) {
|
|
7
|
+
FOR_LOOP_INDEX++
|
|
8
|
+
const [node] = AbstractSyntaxTree.template(`
|
|
9
|
+
(function () {
|
|
10
|
+
var __output__ = [];
|
|
11
|
+
for (var <%= index %> = 0, <%= length %> = <%= array %>.length; <%= index %> < <%= length %>; <%= index %>++) {
|
|
12
|
+
var <%= item %> = <%= array %>[<%= index %>];
|
|
13
|
+
__output__.push(%= children %);
|
|
14
|
+
}
|
|
15
|
+
return __output__;
|
|
16
|
+
})();
|
|
17
|
+
`, {
|
|
18
|
+
index: new Identifier(`__i${FOR_LOOP_INDEX}__`),
|
|
19
|
+
length: new Identifier(`__ilen${FOR_LOOP_INDEX}__`),
|
|
20
|
+
item: new Identifier(htmlNode.attributes[0].key),
|
|
21
|
+
// TODO we should not mark params which were created on the fly, e.g. for nested loops
|
|
22
|
+
array: new Identifier({ name: htmlNode.attributes[2].key, parameter: true }),
|
|
23
|
+
children: htmlNode.children.map((child, index) =>
|
|
24
|
+
transpileNode({ node: child, parent: htmlNode, index: index })
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
return node.expression
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = mapForStatement
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
+
const { transpileExpression } = require('../expression')
|
|
3
|
+
const { isCurlyTag } = require('../../../utilities/string')
|
|
4
|
+
|
|
5
|
+
const { BlockStatement, Literal, Identifier, IfStatement, ReturnStatement } = AbstractSyntaxTree
|
|
6
|
+
|
|
7
|
+
function mapIfStatement (htmlNode, parent, index = 0, transpileNode) {
|
|
8
|
+
function mapAttributesToTest ({ attributes }) {
|
|
9
|
+
if (attributes.length === 1) {
|
|
10
|
+
if (attributes[0].key === 'true' || attributes[0].key === '{true}') {
|
|
11
|
+
return new Literal(true)
|
|
12
|
+
} else if (attributes[0].key === 'false' || attributes[0].key === '{false}') {
|
|
13
|
+
return new Literal(false)
|
|
14
|
+
} else if (isCurlyTag(attributes[0].key)) {
|
|
15
|
+
return transpileExpression(attributes[0].key)
|
|
16
|
+
} else {
|
|
17
|
+
return new Identifier({ name: attributes[0].key, parameter: true })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw new Error('Unsupported length of attributes (if)')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapCurrentNodeToConsequent (htmlNode) {
|
|
24
|
+
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
25
|
+
const argument = body.pop() || new Literal('')
|
|
26
|
+
body.push(new ReturnStatement({ argument }))
|
|
27
|
+
return new BlockStatement({ body })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapNextNodeToAlternate (nextNode) {
|
|
31
|
+
if (nextNode && nextNode.tagName === 'else') {
|
|
32
|
+
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: nextNode.children, index })).filter(Boolean)
|
|
33
|
+
const argument = body.pop()
|
|
34
|
+
body.push(new ReturnStatement({ argument }))
|
|
35
|
+
return new BlockStatement({ body })
|
|
36
|
+
} else if (nextNode && nextNode.tagName === 'elseif') {
|
|
37
|
+
return mapIfStatement(nextNode, parent, index + 1, transpileNode)
|
|
38
|
+
}
|
|
39
|
+
return new BlockStatement({
|
|
40
|
+
body: [
|
|
41
|
+
new ReturnStatement({
|
|
42
|
+
argument: new Literal('')
|
|
43
|
+
})
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new IfStatement({
|
|
49
|
+
test: mapAttributesToTest(htmlNode),
|
|
50
|
+
consequent: mapCurrentNodeToConsequent(htmlNode),
|
|
51
|
+
alternate: mapNextNodeToAlternate(parent[index + 1])
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = mapIfStatement
|
|
@@ -2,5 +2,12 @@ module.exports = {
|
|
|
2
2
|
import: require('./import'),
|
|
3
3
|
doctype: require('./doctype'),
|
|
4
4
|
partial: require('./partial'),
|
|
5
|
-
slot: require('./slot')
|
|
5
|
+
slot: require('./slot'),
|
|
6
|
+
for: require('./for'),
|
|
7
|
+
if: require('./if'),
|
|
8
|
+
unless: require('./unless'),
|
|
9
|
+
try: require('./try'),
|
|
10
|
+
any: require('./any'),
|
|
11
|
+
comment: require('./comment'),
|
|
12
|
+
text: require('./text')
|
|
6
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
-
const { camelize } = require('pure-utilities/string')
|
|
3
2
|
const { findAttributeByKey } = require('../../../utilities/attributes')
|
|
3
|
+
const { pathToIdentifier } = require('../utilities/path')
|
|
4
4
|
|
|
5
5
|
const { CallExpression, Identifier } = AbstractSyntaxTree
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ module.exports = function partial (node) {
|
|
|
9
9
|
const path = attribute.value
|
|
10
10
|
return new CallExpression({
|
|
11
11
|
callee: new Identifier({
|
|
12
|
-
name:
|
|
12
|
+
name: pathToIdentifier(path),
|
|
13
13
|
partial: true,
|
|
14
14
|
path
|
|
15
15
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
+
|
|
3
|
+
const { ReturnStatement, BlockStatement, TryStatement, CatchClause } = AbstractSyntaxTree
|
|
4
|
+
|
|
5
|
+
function mapTryStatement (htmlNode, parent, index, transpileNode) {
|
|
6
|
+
function mapCurrentNodeToBlockStatement (htmlNode) {
|
|
7
|
+
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
8
|
+
const argument = body.pop()
|
|
9
|
+
body.push(new ReturnStatement({ argument }))
|
|
10
|
+
return new BlockStatement({ body })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mapNextNodeToCatchClause (nextNode) {
|
|
14
|
+
if (nextNode && nextNode.tagName === 'catch') {
|
|
15
|
+
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
16
|
+
const argument = body.pop()
|
|
17
|
+
body.push(new ReturnStatement({ argument }))
|
|
18
|
+
return new CatchClause({
|
|
19
|
+
body: new BlockStatement({ body })
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
return new TryStatement({
|
|
25
|
+
block: mapCurrentNodeToBlockStatement(htmlNode),
|
|
26
|
+
handler: mapNextNodeToCatchClause(parent[index + 1])
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = mapTryStatement
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
2
|
+
const ifTag = require('./if')
|
|
3
|
+
|
|
4
|
+
const { Identifier, Literal, IfStatement, ReturnStatement, BlockStatement, UnaryExpression } = AbstractSyntaxTree
|
|
5
|
+
|
|
6
|
+
function mapUnlessStatement (htmlNode, parent, index, transpileNode) {
|
|
7
|
+
function mapAttributesToTest ({ attributes }) {
|
|
8
|
+
if (attributes.length === 1) {
|
|
9
|
+
if (attributes[0].key === 'true' || attributes[0].key === '{true}') {
|
|
10
|
+
return new Literal(true)
|
|
11
|
+
} else if (attributes[0].key === 'false' || attributes[0].key === '{false}') {
|
|
12
|
+
return new Literal(false)
|
|
13
|
+
} else {
|
|
14
|
+
return new Identifier({ name: attributes[0].key, parameter: true })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
throw new Error('Unsupported length of attributes (unless)')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mapCurrentNodeToConsequent (htmlNode) {
|
|
21
|
+
const body = htmlNode.children.map((node, index) => transpileNode({ node, parent: htmlNode.children, index })).filter(Boolean)
|
|
22
|
+
const argument = body.pop()
|
|
23
|
+
body.push(new ReturnStatement({ argument }))
|
|
24
|
+
return new BlockStatement({ body })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mapNextNodeToAlternate (nextNode) {
|
|
28
|
+
if (nextNode && nextNode.tagName === 'else') {
|
|
29
|
+
const body = nextNode.children.map((node, index) => transpileNode({ node, parent: nextNode.children, index })).filter(Boolean)
|
|
30
|
+
const argument = body.pop()
|
|
31
|
+
body.push(new ReturnStatement({ argument }))
|
|
32
|
+
return new BlockStatement({ body })
|
|
33
|
+
} else if (nextNode && nextNode.tagName === 'elseunless') {
|
|
34
|
+
return mapUnlessStatement(nextNode, parent, index + 1, transpileNode)
|
|
35
|
+
} else if (nextNode && nextNode.tagName === 'elseif') {
|
|
36
|
+
return ifTag(nextNode, parent, index + 1, transpileNode)
|
|
37
|
+
}
|
|
38
|
+
return new BlockStatement({
|
|
39
|
+
body: [
|
|
40
|
+
new ReturnStatement({
|
|
41
|
+
argument: new Literal('')
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new IfStatement({
|
|
48
|
+
test: new UnaryExpression({
|
|
49
|
+
operator: '!',
|
|
50
|
+
argument: mapAttributesToTest(htmlNode),
|
|
51
|
+
prefix: true
|
|
52
|
+
}),
|
|
53
|
+
consequent: mapCurrentNodeToConsequent(htmlNode),
|
|
54
|
+
alternate: mapNextNodeToAlternate(parent[index + 1])
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = mapUnlessStatement
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const lexer = require('
|
|
3
|
+
const lexer = require('../lexers/internal')
|
|
4
4
|
const AbstractSyntaxTree = require('abstract-syntax-tree')
|
|
5
5
|
const { isCurlyTag, getTagValue, curlyTag } = require('./string')
|
|
6
6
|
const { parse, stringify } = require('./html')
|
package/src/utilities/string.js
CHANGED
package/src/Linter.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const walk = require('himalaya-walk')
|
|
4
|
-
const { isImportTag } = require('./utilities/string')
|
|
5
|
-
const { unique, duplicates } = require('pure-utilities/array')
|
|
6
|
-
const { getComponentNames } = require('./utilities/attributes')
|
|
7
|
-
const { getAssetPaths, isImageNode } = require('./utilities/node')
|
|
8
|
-
|
|
9
|
-
function isStyleImport (node) {
|
|
10
|
-
const from = node.attributes.find(attr => attr.key === 'from')
|
|
11
|
-
if (from && from.value.endsWith('.css')) {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
return false
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function analyze (tree) {
|
|
18
|
-
const components = []
|
|
19
|
-
walk(tree, node => {
|
|
20
|
-
if (isImportTag(node.tagName) && !isStyleImport(node)) {
|
|
21
|
-
const names = getComponentNames(node.attributes)
|
|
22
|
-
names.forEach(name => components.push(name))
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
return { components: unique(components) }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isExternalUrl (url) {
|
|
29
|
-
if (!url) { return false }
|
|
30
|
-
return url.startsWith('http://') || url.startsWith('https://')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
module.exports = class Linter {
|
|
34
|
-
lint (tree, source, imports = [], options = {}) {
|
|
35
|
-
return [
|
|
36
|
-
...this.verifyTags(tree),
|
|
37
|
-
...this.verifyComponents(tree),
|
|
38
|
-
...this.verifyImports(imports, options)
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
verifyTags (tree) {
|
|
43
|
-
const ANCHOR_TAG = 'a'
|
|
44
|
-
const IMAGE_TAG = 'img'
|
|
45
|
-
const warnings = []
|
|
46
|
-
const { components } = analyze(tree)
|
|
47
|
-
walk(tree, node => {
|
|
48
|
-
if (node.tagName === ANCHOR_TAG && !components.includes(ANCHOR_TAG)) {
|
|
49
|
-
const href = node.attributes.find(attribute => attribute.key === 'href')
|
|
50
|
-
if (href && isExternalUrl(href.value)) {
|
|
51
|
-
const rel = node.attributes.find(attribute => attribute.key === 'rel')
|
|
52
|
-
if (!rel) {
|
|
53
|
-
warnings.push({ message: `${ANCHOR_TAG} tag with external href should have a rel attribute (e.g. rel="noopener")`, type: 'REL_ATTRIBUTE_MISSING' })
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (node.tagName === IMAGE_TAG && !components.includes(IMAGE_TAG)) {
|
|
59
|
-
const alt = node.attributes.find(attribute => attribute.key === 'alt' || attribute.key.startsWith('alt|'))
|
|
60
|
-
if (!alt) {
|
|
61
|
-
warnings.push({ message: `${IMAGE_TAG} tag should have an alt attribute`, type: 'ALT_ATTRIBUTE_MISSING' })
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
return warnings
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
verifyComponents (tree) {
|
|
69
|
-
const warnings = []
|
|
70
|
-
const { components } = analyze(tree)
|
|
71
|
-
walk(tree, node => {
|
|
72
|
-
const index = components.indexOf(node.tagName)
|
|
73
|
-
if (index !== -1) {
|
|
74
|
-
components.splice(index, 1)
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
// TODO: unify warnings.
|
|
78
|
-
components.forEach(component => {
|
|
79
|
-
warnings.push({ type: 'UNUSED_COMPONENT', message: `${component} component is unused` })
|
|
80
|
-
})
|
|
81
|
-
return warnings
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
verifyImports (imports, options) {
|
|
85
|
-
const warnings = []
|
|
86
|
-
const allNames = []
|
|
87
|
-
let allPaths = []
|
|
88
|
-
imports.forEach(node => {
|
|
89
|
-
if (isImportTag(node.tagName)) {
|
|
90
|
-
const names = getComponentNames(node.attributes)
|
|
91
|
-
names.forEach(name => {
|
|
92
|
-
if (allNames.includes(name)) {
|
|
93
|
-
warnings.push({ message: `Component name duplicate: ${name}`, type: 'COMPONENT_NAME_DUPLICATE' })
|
|
94
|
-
} else {
|
|
95
|
-
allNames.push(name)
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
imports.forEach(node => {
|
|
101
|
-
let assetPaths = getAssetPaths(node)
|
|
102
|
-
if (isImageNode(node, options)) {
|
|
103
|
-
assetPaths = assetPaths.filter(item => !assetPaths.includes(item))
|
|
104
|
-
}
|
|
105
|
-
allPaths = allPaths.concat(assetPaths)
|
|
106
|
-
})
|
|
107
|
-
duplicates(allPaths).forEach(duplicate => {
|
|
108
|
-
warnings.push({ message: `Component path duplicate: ${duplicate}`, type: 'COMPONENT_PATH_DUPLICATE' })
|
|
109
|
-
})
|
|
110
|
-
return warnings
|
|
111
|
-
}
|
|
112
|
-
}
|