boxwood 0.60.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 - 2021 buxlabs
3
+ Copyright (c) 2018 - 2022 buxlabs
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "boxwood",
3
- "version": "0.60.1",
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": ">= 12.20.1"
22
+ "node": ">= 16.13.1"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -47,42 +47,42 @@
47
47
  },
48
48
  "homepage": "https://github.com/buxlabs/boxwood#readme",
49
49
  "dependencies": {
50
- "@rollup/plugin-commonjs": "^21.0.0",
51
- "@rollup/plugin-node-resolve": "^13.0.5",
52
- "abstract-syntax-tree": "^2.20.3",
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.22.0",
54
+ "axios": "^0.25.0",
55
55
  "axios-extensions": "3.1.3",
56
56
  "css-tree": "^1.1.3",
57
- "csso": "^4.2.0",
58
- "esbuild": "^0.13.4",
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.0",
61
+ "html-lexer": "^0.4.1",
62
62
  "html-minifier": "4.0.0",
63
63
  "memoizee": "0.4.15",
64
64
  "negate-sentence": "0.2.0",
65
65
  "path-to-regexp": "6.2.0",
66
66
  "pure-conditions": "1.2.1",
67
- "pure-utilities": "^1.2.3",
68
- "rollup": "^2.58.0",
67
+ "pure-utilities": "^1.2.4",
68
+ "rollup": "^2.64.0",
69
69
  "rollup-plugin-includepaths": "0.2.4",
70
70
  "string-hash": "1.1.3",
71
71
  "yaml": "^1.10.2"
72
72
  },
73
73
  "devDependencies": {
74
- "ava": "3.15.0",
74
+ "ava": "^4.0.1",
75
75
  "benchmark": "2.1.4",
76
76
  "browser-env": "3.3.0",
77
- "express": "4.17.1",
77
+ "express": "^4.17.2",
78
78
  "handlebars": "^4.7.7",
79
79
  "lodash.template": "4.5.0",
80
80
  "mustache": "^4.2.0",
81
81
  "nyc": "15.1.0",
82
- "puppeteer": "^10.4.0",
82
+ "puppeteer": "^13.1.1",
83
83
  "standard": "^16.0.4",
84
- "typescript": "^4.4.3",
85
- "underscore": "^1.13.1"
84
+ "typescript": "^4.5.4",
85
+ "underscore": "^1.13.2"
86
86
  },
87
87
  "standard": {
88
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 Linter = require('./Linter')
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 warnings = linter.lint(tree, source, imports.map(({ node }) => node), options)
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 = new AbstractSyntaxTree(input)
13
- let scoped = false
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
@@ -6,7 +6,7 @@ const END_TAG = '}'
6
6
  const TEXT = 'text'
7
7
  const EXPRESSION = 'expression'
8
8
 
9
- module.exports = function lexer (input) {
9
+ module.exports = function tokenize (input) {
10
10
  const tokens = []
11
11
  const length = input.length
12
12
  let index = 0
@@ -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 }
@@ -1,6 +1,6 @@
1
1
  const AbstractSyntaxTree = require('abstract-syntax-tree')
2
2
  const { unique } = require('pure-utilities/array')
3
- const lexer = require('../../utilities/lexer')
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
- function mapAttributes (attributes) {
50
- function getAttributeValue (value) {
51
- if (value === null) { return new Literal(true) }
52
- return transpileExpression(value, false)
53
- }
54
- return attributes.length > 0
55
- ? new ObjectExpression({
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 (htmlNode.type === 'element' && htmlNode.tagName === 'else') {
16
+ } else if (tagName === 'else') {
204
17
  return null
205
- } else if (htmlNode.type === 'element' && htmlNode.tagName === 'elseif') {
18
+ } else if (tagName === 'elseif') {
206
19
  return null
207
- } else if (htmlNode.type === 'element' && htmlNode.tagName === 'unless') {
208
- const statement = mapUnlessStatement(htmlNode, parent, index)
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 (htmlNode.type === 'element' && htmlNode.tagName === 'elseunless') {
24
+ } else if (tagName === 'elseunless') {
212
25
  return null
213
- } else if (htmlNode.type === 'element' && htmlNode.tagName === 'try') {
214
- const statement = mapTryStatement(htmlNode, parent, index)
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 (htmlNode.type === 'element' && htmlNode.tagName === 'catch') {
30
+ } else if (tagName === 'catch') {
218
31
  return null
219
- } else if (htmlNode.type === 'element' && htmlNode.tagName === 'for') {
220
- return mapForStatement(htmlNode, parent, index)
221
- } else if (htmlNode.type === 'element') {
222
- if (htmlNode.tagName === 'import') { return tags.import(htmlNode) }
223
- if (htmlNode.tagName === '!doctype') { return tags.doctype() }
224
- if (htmlNode.tagName === 'partial') { return tags.partial(htmlNode) }
225
- if (htmlNode.tagName === 'slot') { return tags.slot(htmlNode) }
226
- const { tagName, attributes, children } = htmlNode
227
- const node = new CallExpression({
228
- callee: new Identifier('tag'),
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,7 @@
1
+ const AbstractSyntaxTree = require('abstract-syntax-tree')
2
+
3
+ const { Literal } = AbstractSyntaxTree
4
+
5
+ module.exports = function () {
6
+ return new Literal('')
7
+ }
@@ -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: `__${camelize(path)}__`,
12
+ name: pathToIdentifier(path),
13
13
  partial: true,
14
14
  path
15
15
  })
@@ -0,0 +1,6 @@
1
+ const { transpileExpression } = require('../expression')
2
+
3
+ module.exports = function (htmlNode) {
4
+ const { content } = htmlNode
5
+ return transpileExpression(content)
6
+ }
@@ -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
@@ -0,0 +1,9 @@
1
+ const { camelize } = require('pure-utilities/string')
2
+
3
+ function pathToIdentifier (path) {
4
+ return `__${camelize(path.replace(/\./g, 'Dot_').replace(/\//g, 'Slash_'))}__`
5
+ }
6
+
7
+ module.exports = {
8
+ pathToIdentifier
9
+ }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const lexer = require('./lexer')
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')
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { extname } = require('path')
4
- const lexer = require('./lexer')
4
+ const lexer = require('../lexers/internal')
5
5
  const stringHash = require('string-hash')
6
6
 
7
7
  function curlyTag (string) {
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
- }