Initial tests set up (#10)
* Begin debugging * Initial tests set up This adds tests using uvu (we can switch if people want) and restructures things a bit so that it's easier to test. Like in snowpack you set up a little project. In our tests you can say: ```js const result = await runtime.load('/blog/hello-world') ``` And analyze the result. I included a `test-helpers.js` which has a function that will turn HTML into a cheerio instance, for inspecting the result HTML. * Add CI * Remove extra console logs * Formatting
This commit is contained in:
parent
8ebc077cb0
commit
17c3c98f07
40 changed files with 4199 additions and 4026 deletions
26
.github/workflows/nodejs.yml
vendored
Normal file
26
.github/workflows/nodejs.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: Node CI
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- name: npm install, build, and test
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
npm test
|
||||||
|
env:
|
||||||
|
CI: true
|
289
package-lock.json
generated
289
package-lock.json
generated
|
@ -536,7 +536,8 @@
|
||||||
"boolbase": {
|
"boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"boxen": {
|
"boxen": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
|
@ -684,67 +685,31 @@
|
||||||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
|
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
|
||||||
},
|
},
|
||||||
"cheerio": {
|
"cheerio": {
|
||||||
"version": "0.22.0",
|
"version": "1.0.0-rc.5",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz",
|
||||||
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
|
"integrity": "sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"css-select": "~1.2.0",
|
"cheerio-select-tmp": "^0.1.0",
|
||||||
"dom-serializer": "~0.1.0",
|
"dom-serializer": "~1.2.0",
|
||||||
"entities": "~1.1.1",
|
"domhandler": "^4.0.0",
|
||||||
"htmlparser2": "^3.9.1",
|
"entities": "~2.1.0",
|
||||||
"lodash.assignin": "^4.0.9",
|
"htmlparser2": "^6.0.0",
|
||||||
"lodash.bind": "^4.1.4",
|
"parse5": "^6.0.0",
|
||||||
"lodash.defaults": "^4.0.1",
|
"parse5-htmlparser2-tree-adapter": "^6.0.0"
|
||||||
"lodash.filter": "^4.4.0",
|
}
|
||||||
"lodash.flatten": "^4.2.0",
|
},
|
||||||
"lodash.foreach": "^4.3.0",
|
"cheerio-select-tmp": {
|
||||||
"lodash.map": "^4.4.0",
|
"version": "0.1.1",
|
||||||
"lodash.merge": "^4.4.0",
|
"resolved": "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz",
|
||||||
"lodash.pick": "^4.2.1",
|
"integrity": "sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==",
|
||||||
"lodash.reduce": "^4.4.0",
|
"dev": true,
|
||||||
"lodash.reject": "^4.4.0",
|
"requires": {
|
||||||
"lodash.some": "^4.4.0"
|
"css-select": "^3.1.2",
|
||||||
},
|
"css-what": "^4.0.0",
|
||||||
"dependencies": {
|
"domelementtype": "^2.1.0",
|
||||||
"domhandler": {
|
"domhandler": "^4.0.0",
|
||||||
"version": "2.4.2",
|
"domutils": "^2.4.4"
|
||||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
|
|
||||||
"requires": {
|
|
||||||
"domelementtype": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"htmlparser2": {
|
|
||||||
"version": "3.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
|
||||||
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
|
|
||||||
"requires": {
|
|
||||||
"domelementtype": "^1.3.1",
|
|
||||||
"domhandler": "^2.3.0",
|
|
||||||
"domutils": "^1.5.1",
|
|
||||||
"entities": "^1.1.1",
|
|
||||||
"inherits": "^2.0.1",
|
|
||||||
"readable-stream": "^3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
|
||||||
"requires": {
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"string_decoder": "^1.1.1",
|
|
||||||
"util-deprecate": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "~5.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
|
@ -950,14 +915,16 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"css-select": {
|
"css-select": {
|
||||||
"version": "1.2.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz",
|
||||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
"integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"boolbase": "~1.0.0",
|
"boolbase": "^1.0.0",
|
||||||
"css-what": "2.1",
|
"css-what": "^4.0.0",
|
||||||
"domutils": "1.5.1",
|
"domhandler": "^4.0.0",
|
||||||
"nth-check": "~1.0.1"
|
"domutils": "^2.4.3",
|
||||||
|
"nth-check": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"css-tree": {
|
"css-tree": {
|
||||||
|
@ -970,9 +937,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"css-what": {
|
"css-what": {
|
||||||
"version": "2.1.3",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz",
|
||||||
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="
|
"integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"cssesc": {
|
"cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
@ -1019,6 +987,11 @@
|
||||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||||
|
},
|
||||||
"default-browser-id": {
|
"default-browser-id": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz",
|
||||||
|
@ -1045,6 +1018,18 @@
|
||||||
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
|
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"dequal": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"dir-glob": {
|
"dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
|
@ -1064,18 +1049,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dom-serializer": {
|
"dom-serializer": {
|
||||||
"version": "0.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
|
||||||
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
|
"integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"domelementtype": "^1.3.0",
|
"domelementtype": "^2.0.1",
|
||||||
"entities": "^1.1.1"
|
"domhandler": "^4.0.0",
|
||||||
|
"entities": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domelementtype": {
|
"domelementtype": {
|
||||||
"version": "1.3.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
|
||||||
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
|
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"domhandler": {
|
"domhandler": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
@ -1093,12 +1081,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domutils": {
|
"domutils": {
|
||||||
"version": "1.5.1",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.0.tgz",
|
||||||
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
|
"integrity": "sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"dom-serializer": "0",
|
"dom-serializer": "^1.0.1",
|
||||||
"domelementtype": "1"
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dot-prop": {
|
"dot-prop": {
|
||||||
|
@ -1151,9 +1141,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entities": {
|
"entities": {
|
||||||
"version": "1.1.2",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
|
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"error-ex": {
|
"error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
|
@ -1806,7 +1797,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.7",
|
"version": "1.3.7",
|
||||||
|
@ -2073,71 +2065,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
},
|
},
|
||||||
"lodash.assignin": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI="
|
|
||||||
},
|
|
||||||
"lodash.bind": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
|
|
||||||
"integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU="
|
|
||||||
},
|
|
||||||
"lodash.camelcase": {
|
"lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||||
},
|
},
|
||||||
"lodash.defaults": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
|
||||||
},
|
|
||||||
"lodash.filter": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
|
|
||||||
"integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4="
|
|
||||||
},
|
|
||||||
"lodash.flatten": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
|
||||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
|
||||||
},
|
|
||||||
"lodash.foreach": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
|
||||||
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
|
|
||||||
},
|
|
||||||
"lodash.map": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
|
|
||||||
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM="
|
|
||||||
},
|
|
||||||
"lodash.merge": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
|
||||||
},
|
|
||||||
"lodash.pick": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
|
|
||||||
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
|
|
||||||
},
|
|
||||||
"lodash.reduce": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
|
|
||||||
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
|
|
||||||
},
|
|
||||||
"lodash.reject": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz",
|
|
||||||
"integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU="
|
|
||||||
},
|
|
||||||
"lodash.some": {
|
|
||||||
"version": "4.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
|
|
||||||
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0="
|
|
||||||
},
|
|
||||||
"loose-envify": {
|
"loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
@ -2292,6 +2224,12 @@
|
||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"mri": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -2390,11 +2328,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"nth-check": {
|
"nth-check": {
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
|
||||||
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
|
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"boolbase": "~1.0.0"
|
"boolbase": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
|
@ -2499,6 +2438,21 @@
|
||||||
"lines-and-columns": "^1.1.6"
|
"lines-and-columns": "^1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse5": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"parse5-htmlparser2-tree-adapter": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"parse5": "^6.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"path-is-absolute": {
|
"path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
@ -2873,10 +2827,14 @@
|
||||||
"tslib": "^1.9.0"
|
"tslib": "^1.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"sade": {
|
||||||
"version": "5.2.1",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
"integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"mri": "^1.1.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sass": {
|
"sass": {
|
||||||
"version": "1.32.8",
|
"version": "1.32.8",
|
||||||
|
@ -3216,6 +3174,12 @@
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"totalist": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"touch": {
|
"touch": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||||
|
@ -3366,6 +3330,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
},
|
},
|
||||||
|
"uvu": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-JGxttnOGDFs77FaZ0yMUHIzczzQ5R1IlDeNW6Wymw6gAscwMdAffVOP6TlxLIfReZyK8tahoGwWZaTCJzNFDkg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dequal": "^2.0.0",
|
||||||
|
"diff": "^5.0.0",
|
||||||
|
"kleur": "^4.0.3",
|
||||||
|
"sade": "^1.7.3",
|
||||||
|
"totalist": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"v8-compile-cache": {
|
"v8-compile-cache": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
"dev": "concurrently 'tsc --watch' 'npm run copy-js:watch'",
|
"dev": "concurrently 'tsc --watch' 'npm run copy-js:watch'",
|
||||||
"format": "prettier -w 'src/**/*.{js,ts}'",
|
"format": "prettier -w 'src/**/*.{js,ts}'",
|
||||||
"copy-js": "copyfiles -u 1 src/*.js lib/",
|
"copy-js": "copyfiles -u 1 src/*.js lib/",
|
||||||
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'"
|
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'",
|
||||||
|
"test": "uvu test -i fixtures -i test-utils.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "0.0.46",
|
"@types/estree": "0.0.46",
|
||||||
|
@ -31,8 +32,8 @@
|
||||||
"acorn-jsx": "^5.3.1",
|
"acorn-jsx": "^5.3.1",
|
||||||
"astring": "^1.7.0",
|
"astring": "^1.7.0",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
"cheerio": "^0.22.0",
|
|
||||||
"css-tree": "^1.1.2",
|
"css-tree": "^1.1.2",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
"domhandler": "^4.0.0",
|
"domhandler": "^4.0.0",
|
||||||
"es-module-lexer": "^0.4.1",
|
"es-module-lexer": "^0.4.1",
|
||||||
"gray-matter": "^4.0.2",
|
"gray-matter": "^4.0.2",
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
"@types/yargs-parser": "^20.2.0",
|
"@types/yargs-parser": "^20.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^4.18.0",
|
||||||
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"concurrently": "^6.0.0",
|
"concurrently": "^6.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"eslint": "^7.22.0",
|
"eslint": "^7.22.0",
|
||||||
|
@ -67,6 +69,7 @@
|
||||||
"preact": "^10.5.12",
|
"preact": "^10.5.12",
|
||||||
"preact-render-to-string": "^5.1.14",
|
"preact-render-to-string": "^5.1.14",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"typescript": "^4.2.3"
|
"typescript": "^4.2.3",
|
||||||
|
"uvu": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/cli.ts
23
src/cli.ts
|
@ -1,10 +1,10 @@
|
||||||
import type { AstroConfig } from './@types/astro';
|
import type { AstroConfig } from './@types/astro';
|
||||||
|
|
||||||
import * as colors from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { existsSync, promises as fsPromises } from 'fs';
|
|
||||||
import yargs from 'yargs-parser';
|
import yargs from 'yargs-parser';
|
||||||
|
|
||||||
|
import { loadConfig } from './config.js';
|
||||||
import generate from './generate.js';
|
import generate from './generate.js';
|
||||||
import devServer from './dev.js';
|
import devServer from './dev.js';
|
||||||
|
|
||||||
|
@ -49,25 +49,6 @@ async function printVersion() {
|
||||||
console.error(pkg.version);
|
console.error(pkg.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
|
|
||||||
if (typeof rawRoot === 'undefined') {
|
|
||||||
rawRoot = process.cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = pathResolve(rawRoot);
|
|
||||||
const fileProtocolRoot = `file://${root}/`;
|
|
||||||
const astroConfigPath = pathJoin(root, 'astro.config.mjs');
|
|
||||||
|
|
||||||
if (!existsSync(astroConfigPath)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const astroConfig: AstroConfig = (await import(astroConfigPath)).default;
|
|
||||||
astroConfig.projectRoot = new URL(astroConfig.projectRoot + '/', fileProtocolRoot);
|
|
||||||
astroConfig.hmxRoot = new URL(astroConfig.hmxRoot + '/', fileProtocolRoot);
|
|
||||||
return astroConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>) {
|
async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>) {
|
||||||
const astroConfig = await loadConfig(rawRoot);
|
const astroConfig = await loadConfig(rawRoot);
|
||||||
if (typeof astroConfig === 'undefined') {
|
if (typeof astroConfig === 'undefined') {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CompileOptions } from '../@types/compiler';
|
import type { CompileOptions } from '../@types/compiler';
|
||||||
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
import type { Ast, TemplateNode } from '../compiler/interfaces';
|
||||||
import type { JsxItem, TransformResult } from '../@types/astro.js';
|
import type { JsxItem, TransformResult } from '../@types/astro';
|
||||||
|
|
||||||
import eslexer from 'es-module-lexer';
|
import eslexer from 'es-module-lexer';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
|
@ -61,7 +61,6 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
||||||
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
|
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
console.log(val);
|
|
||||||
throw new Error('UNKNOWN V');
|
throw new Error('UNKNOWN V');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +74,6 @@ function getTextFromAttribute(attr: any): string {
|
||||||
if (attr.data !== undefined) {
|
if (attr.data !== undefined) {
|
||||||
return attr.data;
|
return attr.data;
|
||||||
}
|
}
|
||||||
console.log(attr);
|
|
||||||
throw new Error('UNKNOWN attr');
|
throw new Error('UNKNOWN attr');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,12 +167,11 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
|
||||||
|
|
||||||
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
|
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
|
||||||
await eslexer.init;
|
await eslexer.init;
|
||||||
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
|
|
||||||
|
|
||||||
// Compile scripts as TypeScript, always
|
// Compile scripts as TypeScript, always
|
||||||
|
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
|
||||||
|
|
||||||
// Todo: Validate that `h` and `Fragment` aren't defined in the script
|
// Todo: Validate that `h` and `Fragment` aren't defined in the script
|
||||||
|
|
||||||
const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
|
const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
|
||||||
const components = Object.fromEntries(
|
const components = Object.fromEntries(
|
||||||
scriptImports.map((imp) => {
|
scriptImports.map((imp) => {
|
||||||
|
@ -193,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
|
|
||||||
walk(ast.html, {
|
walk(ast.html, {
|
||||||
enter(node: TemplateNode) {
|
enter(node: TemplateNode) {
|
||||||
// console.log("enter", node.type);
|
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'MustacheTag':
|
case 'MustacheTag':
|
||||||
let code = compileScriptSafe(node.expression, 'jsx');
|
let code = compileScriptSafe(node.expression, 'jsx');
|
||||||
|
@ -238,7 +234,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
case 'Element':
|
case 'Element':
|
||||||
const name: string = node.name;
|
const name: string = node.name;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
console.log(node);
|
|
||||||
throw new Error('AHHHH');
|
throw new Error('AHHHH');
|
||||||
}
|
}
|
||||||
const attributes = getAttributes(node.attributes);
|
const attributes = getAttributes(node.attributes);
|
||||||
|
@ -298,12 +293,10 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
console.log(node);
|
|
||||||
throw new Error('Unexpected node type: ' + node.type);
|
throw new Error('Unexpected node type: ' + node.type);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
// console.log("leave", node.type);
|
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'Text':
|
case 'Text':
|
||||||
case 'MustacheTag':
|
case 'MustacheTag':
|
||||||
|
|
|
@ -1,75 +1,82 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
const now = (typeof process !== 'undefined' && process.hrtime)
|
const now =
|
||||||
? () => {
|
typeof process !== 'undefined' && process.hrtime
|
||||||
const t = process.hrtime();
|
? () => {
|
||||||
return t[0] * 1e3 + t[1] / 1e6;
|
const t = process.hrtime();
|
||||||
}
|
return t[0] * 1e3 + t[1] / 1e6;
|
||||||
: () => self.performance.now();
|
}
|
||||||
|
: () => self.performance.now();
|
||||||
|
|
||||||
interface Timing {
|
interface Timing {
|
||||||
label: string;
|
label: string;
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
children: Timing[];
|
children: Timing[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapse_timings(timings) {
|
function collapse_timings(timings) {
|
||||||
const result = {};
|
const result = {};
|
||||||
timings.forEach(timing => {
|
timings.forEach((timing) => {
|
||||||
result[timing.label] = Object.assign({
|
result[timing.label] = Object.assign(
|
||||||
total: timing.end - timing.start
|
{
|
||||||
}, timing.children && collapse_timings(timing.children));
|
total: timing.end - timing.start,
|
||||||
});
|
},
|
||||||
return result;
|
timing.children && collapse_timings(timing.children)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Stats {
|
export default class Stats {
|
||||||
start_time: number;
|
start_time: number;
|
||||||
current_timing: Timing;
|
current_timing: Timing;
|
||||||
current_children: Timing[];
|
current_children: Timing[];
|
||||||
timings: Timing[];
|
timings: Timing[];
|
||||||
stack: Timing[];
|
stack: Timing[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.start_time = now();
|
this.start_time = now();
|
||||||
this.stack = [];
|
this.stack = [];
|
||||||
this.current_children = this.timings = [];
|
this.current_children = this.timings = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
start(label) {
|
start(label) {
|
||||||
const timing = {
|
const timing = {
|
||||||
label,
|
label,
|
||||||
start: now(),
|
start: now(),
|
||||||
end: null,
|
end: null,
|
||||||
children: []
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.current_children.push(timing);
|
this.current_children.push(timing);
|
||||||
this.stack.push(timing);
|
this.stack.push(timing);
|
||||||
|
|
||||||
this.current_timing = timing;
|
this.current_timing = timing;
|
||||||
this.current_children = timing.children;
|
this.current_children = timing.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(label) {
|
stop(label) {
|
||||||
if (label !== this.current_timing.label) {
|
if (label !== this.current_timing.label) {
|
||||||
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
|
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.current_timing.end = now();
|
this.current_timing.end = now();
|
||||||
this.stack.pop();
|
this.stack.pop();
|
||||||
this.current_timing = this.stack[this.stack.length - 1];
|
this.current_timing = this.stack[this.stack.length - 1];
|
||||||
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
|
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const timings = Object.assign({
|
const timings = Object.assign(
|
||||||
total: now() - this.start_time
|
{
|
||||||
}, collapse_timings(this.timings));
|
total: now() - this.start_time,
|
||||||
|
},
|
||||||
|
collapse_timings(this.timings)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timings
|
timings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,16 @@ import jsx from 'acorn-jsx';
|
||||||
|
|
||||||
const acornJsx = acorn.Parser.extend(jsx());
|
const acornJsx = acorn.Parser.extend(jsx());
|
||||||
|
|
||||||
export const parse = (source: string): Node => acorn.parse(source, {
|
export const parse = (source: string): Node =>
|
||||||
sourceType: 'module',
|
acorn.parse(source, {
|
||||||
ecmaVersion: 2020,
|
sourceType: 'module',
|
||||||
locations: true
|
ecmaVersion: 2020,
|
||||||
});
|
locations: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, {
|
export const parse_expression_at = (source: string, index: number): Node =>
|
||||||
sourceType: 'module',
|
acornJsx.parseExpressionAt(source, index, {
|
||||||
ecmaVersion: 2020,
|
sourceType: 'module',
|
||||||
locations: true
|
ecmaVersion: 2020,
|
||||||
});
|
locations: true,
|
||||||
|
});
|
||||||
|
|
|
@ -8,245 +8,257 @@ import full_char_code_at from '../utils/full_char_code_at.js';
|
||||||
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js';
|
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js';
|
||||||
import error from '../utils/error.js';
|
import error from '../utils/error.js';
|
||||||
|
|
||||||
type ParserState = (parser: Parser) => (ParserState | void);
|
type ParserState = (parser: Parser) => ParserState | void;
|
||||||
|
|
||||||
interface LastAutoClosedTag {
|
interface LastAutoClosedTag {
|
||||||
tag: string;
|
tag: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
readonly template: string;
|
readonly template: string;
|
||||||
readonly filename?: string;
|
readonly filename?: string;
|
||||||
readonly customElement: boolean;
|
readonly customElement: boolean;
|
||||||
|
|
||||||
index = 0;
|
index = 0;
|
||||||
stack: TemplateNode[] = [];
|
stack: TemplateNode[] = [];
|
||||||
|
|
||||||
html: Fragment;
|
html: Fragment;
|
||||||
css: Style[] = [];
|
css: Style[] = [];
|
||||||
js: Script[] = [];
|
js: Script[] = [];
|
||||||
meta_tags = {};
|
meta_tags = {};
|
||||||
last_auto_closed_tag?: LastAutoClosedTag;
|
last_auto_closed_tag?: LastAutoClosedTag;
|
||||||
|
|
||||||
constructor(template: string, options: ParserOptions) {
|
constructor(template: string, options: ParserOptions) {
|
||||||
if (typeof template !== 'string') {
|
if (typeof template !== 'string') {
|
||||||
throw new TypeError('Template must be a string');
|
throw new TypeError('Template must be a string');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.template = template.replace(/\s+$/, '');
|
this.template = template.replace(/\s+$/, '');
|
||||||
this.filename = options.filename;
|
this.filename = options.filename;
|
||||||
this.customElement = options.customElement;
|
this.customElement = options.customElement;
|
||||||
|
|
||||||
this.html = {
|
this.html = {
|
||||||
start: null,
|
start: null,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'Fragment',
|
type: 'Fragment',
|
||||||
children: []
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.stack.push(this.html);
|
this.stack.push(this.html);
|
||||||
|
|
||||||
let state: ParserState = fragment;
|
let state: ParserState = fragment;
|
||||||
|
|
||||||
while (this.index < this.template.length) {
|
while (this.index < this.template.length) {
|
||||||
state = state(this) || fragment;
|
state = state(this) || fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stack.length > 1) {
|
if (this.stack.length > 1) {
|
||||||
const current = this.current();
|
const current = this.current();
|
||||||
|
|
||||||
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
|
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
|
||||||
const slug = current.type === 'Element' ? 'element' : 'block';
|
const slug = current.type === 'Element' ? 'element' : 'block';
|
||||||
|
|
||||||
this.error({
|
this.error(
|
||||||
code: `unclosed-${slug}`,
|
{
|
||||||
message: `${type} was left open`
|
code: `unclosed-${slug}`,
|
||||||
}, current.start);
|
message: `${type} was left open`,
|
||||||
}
|
},
|
||||||
|
current.start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (state !== fragment) {
|
if (state !== fragment) {
|
||||||
this.error({
|
this.error({
|
||||||
code: 'unexpected-eof',
|
code: 'unexpected-eof',
|
||||||
message: 'Unexpected end of input'
|
message: 'Unexpected end of input',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.html.children.length) {
|
if (this.html.children.length) {
|
||||||
let start = this.html.children[0].start;
|
let start = this.html.children[0].start;
|
||||||
while (whitespace.test(template[start])) start += 1;
|
while (whitespace.test(template[start])) start += 1;
|
||||||
|
|
||||||
let end = this.html.children[this.html.children.length - 1].end;
|
let end = this.html.children[this.html.children.length - 1].end;
|
||||||
while (whitespace.test(template[end - 1])) end -= 1;
|
while (whitespace.test(template[end - 1])) end -= 1;
|
||||||
|
|
||||||
this.html.start = start;
|
this.html.start = start;
|
||||||
this.html.end = end;
|
this.html.end = end;
|
||||||
} else {
|
} else {
|
||||||
this.html.start = this.html.end = null;
|
this.html.start = this.html.end = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current() {
|
current() {
|
||||||
return this.stack[this.stack.length - 1];
|
return this.stack[this.stack.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
acorn_error(err: any) {
|
acorn_error(err: any) {
|
||||||
this.error({
|
this.error(
|
||||||
code: 'parse-error',
|
{
|
||||||
message: err.message.replace(/ \(\d+:\d+\)$/, '')
|
code: 'parse-error',
|
||||||
}, err.pos);
|
message: err.message.replace(/ \(\d+:\d+\)$/, ''),
|
||||||
}
|
},
|
||||||
|
err.pos
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
error({ code, message }: { code: string; message: string }, index = this.index) {
|
error({ code, message }: { code: string; message: string }, index = this.index) {
|
||||||
error(message, {
|
error(message, {
|
||||||
name: 'ParseError',
|
name: 'ParseError',
|
||||||
code,
|
code,
|
||||||
source: this.template,
|
source: this.template,
|
||||||
start: index,
|
start: index,
|
||||||
filename: this.filename
|
filename: this.filename,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
eat(str: string, required?: boolean, message?: string) {
|
eat(str: string, required?: boolean, message?: string) {
|
||||||
if (this.match(str)) {
|
if (this.match(str)) {
|
||||||
this.index += str.length;
|
this.index += str.length;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (required) {
|
if (required) {
|
||||||
this.error({
|
this.error({
|
||||||
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
|
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
|
||||||
message: message || `Expected ${str}`
|
message: message || `Expected ${str}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
match(str: string) {
|
match(str: string) {
|
||||||
return this.template.slice(this.index, this.index + str.length) === str;
|
return this.template.slice(this.index, this.index + str.length) === str;
|
||||||
}
|
}
|
||||||
|
|
||||||
match_regex(pattern: RegExp) {
|
match_regex(pattern: RegExp) {
|
||||||
const match = pattern.exec(this.template.slice(this.index));
|
const match = pattern.exec(this.template.slice(this.index));
|
||||||
if (!match || match.index !== 0) return null;
|
if (!match || match.index !== 0) return null;
|
||||||
|
|
||||||
return match[0];
|
return match[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_whitespace() {
|
allow_whitespace() {
|
||||||
while (
|
while (this.index < this.template.length && whitespace.test(this.template[this.index])) {
|
||||||
this.index < this.template.length &&
|
this.index++;
|
||||||
whitespace.test(this.template[this.index])
|
}
|
||||||
) {
|
}
|
||||||
this.index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read(pattern: RegExp) {
|
read(pattern: RegExp) {
|
||||||
const result = this.match_regex(pattern);
|
const result = this.match_regex(pattern);
|
||||||
if (result) this.index += result.length;
|
if (result) this.index += result.length;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
read_identifier(allow_reserved = false) {
|
read_identifier(allow_reserved = false) {
|
||||||
const start = this.index;
|
const start = this.index;
|
||||||
|
|
||||||
let i = this.index;
|
let i = this.index;
|
||||||
|
|
||||||
const code = full_char_code_at(this.template, i);
|
const code = full_char_code_at(this.template, i);
|
||||||
if (!isIdentifierStart(code, true)) return null;
|
if (!isIdentifierStart(code, true)) return null;
|
||||||
|
|
||||||
i += code <= 0xffff ? 1 : 2;
|
i += code <= 0xffff ? 1 : 2;
|
||||||
|
|
||||||
while (i < this.template.length) {
|
while (i < this.template.length) {
|
||||||
const code = full_char_code_at(this.template, i);
|
const code = full_char_code_at(this.template, i);
|
||||||
|
|
||||||
if (!isIdentifierChar(code, true)) break;
|
if (!isIdentifierChar(code, true)) break;
|
||||||
i += code <= 0xffff ? 1 : 2;
|
i += code <= 0xffff ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identifier = this.template.slice(this.index, this.index = i);
|
const identifier = this.template.slice(this.index, (this.index = i));
|
||||||
|
|
||||||
if (!allow_reserved && reserved.has(identifier)) {
|
if (!allow_reserved && reserved.has(identifier)) {
|
||||||
this.error({
|
this.error(
|
||||||
code: 'unexpected-reserved-word',
|
{
|
||||||
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
|
code: 'unexpected-reserved-word',
|
||||||
}, start);
|
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
read_until(pattern: RegExp) {
|
read_until(pattern: RegExp) {
|
||||||
if (this.index >= this.template.length) {
|
if (this.index >= this.template.length) {
|
||||||
this.error({
|
this.error({
|
||||||
code: 'unexpected-eof',
|
code: 'unexpected-eof',
|
||||||
message: 'Unexpected end of input'
|
message: 'Unexpected end of input',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = this.index;
|
const start = this.index;
|
||||||
const match = pattern.exec(this.template.slice(start));
|
const match = pattern.exec(this.template.slice(start));
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
this.index = start + match.index;
|
this.index = start + match.index;
|
||||||
return this.template.slice(start, this.index);
|
return this.template.slice(start, this.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.index = this.template.length;
|
this.index = this.template.length;
|
||||||
return this.template.slice(start);
|
return this.template.slice(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
require_whitespace() {
|
require_whitespace() {
|
||||||
if (!whitespace.test(this.template[this.index])) {
|
if (!whitespace.test(this.template[this.index])) {
|
||||||
this.error({
|
this.error({
|
||||||
code: 'missing-whitespace',
|
code: 'missing-whitespace',
|
||||||
message: 'Expected whitespace'
|
message: 'Expected whitespace',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allow_whitespace();
|
this.allow_whitespace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function parse(
|
export default function parse(template: string, options: ParserOptions = {}): Ast {
|
||||||
template: string,
|
const parser = new Parser(template, options);
|
||||||
options: ParserOptions = {}
|
|
||||||
): Ast {
|
|
||||||
const parser = new Parser(template, options);
|
|
||||||
|
|
||||||
// TODO we may want to allow multiple <style> tags —
|
// TODO we may want to allow multiple <style> tags —
|
||||||
// one scoped, one global. for now, only allow one
|
// one scoped, one global. for now, only allow one
|
||||||
if (parser.css.length > 1) {
|
if (parser.css.length > 1) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'duplicate-style',
|
{
|
||||||
message: 'You can only have one top-level <style> tag per component'
|
code: 'duplicate-style',
|
||||||
}, parser.css[1].start);
|
message: 'You can only have one top-level <style> tag per component',
|
||||||
}
|
},
|
||||||
|
parser.css[1].start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const instance_scripts = parser.js.filter(script => script.context === 'default');
|
const instance_scripts = parser.js.filter((script) => script.context === 'default');
|
||||||
const module_scripts = parser.js.filter(script => script.context === 'module');
|
const module_scripts = parser.js.filter((script) => script.context === 'module');
|
||||||
|
|
||||||
if (instance_scripts.length > 1) {
|
if (instance_scripts.length > 1) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-script',
|
{
|
||||||
message: 'A component can only have one instance-level <script> element'
|
code: 'invalid-script',
|
||||||
}, instance_scripts[1].start);
|
message: 'A component can only have one instance-level <script> element',
|
||||||
}
|
},
|
||||||
|
instance_scripts[1].start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (module_scripts.length > 1) {
|
if (module_scripts.length > 1) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-script',
|
{
|
||||||
message: 'A component can only have one <script context="module"> element'
|
code: 'invalid-script',
|
||||||
}, module_scripts[1].start);
|
message: 'A component can only have one <script context="module"> element',
|
||||||
}
|
},
|
||||||
|
module_scripts[1].start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: parser.html,
|
html: parser.html,
|
||||||
css: parser.css[0],
|
css: parser.css[0],
|
||||||
instance: instance_scripts[0],
|
instance: instance_scripts[0],
|
||||||
module: module_scripts[0]
|
module: module_scripts[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,82 +3,70 @@
|
||||||
import { Parser } from '../index.js';
|
import { Parser } from '../index.js';
|
||||||
import { isIdentifierStart } from 'acorn';
|
import { isIdentifierStart } from 'acorn';
|
||||||
import full_char_code_at from '../../utils/full_char_code_at.js';
|
import full_char_code_at from '../../utils/full_char_code_at.js';
|
||||||
import {
|
import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js';
|
||||||
is_bracket_open,
|
|
||||||
is_bracket_close,
|
|
||||||
is_bracket_pair,
|
|
||||||
get_bracket_close
|
|
||||||
} from '../utils/bracket.js';
|
|
||||||
import { parse_expression_at } from '../acorn.js';
|
import { parse_expression_at } from '../acorn.js';
|
||||||
import { Pattern } from 'estree';
|
import { Pattern } from 'estree';
|
||||||
|
|
||||||
export default function read_context(
|
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
|
||||||
parser: Parser
|
const start = parser.index;
|
||||||
): Pattern & { start: number; end: number } {
|
let i = parser.index;
|
||||||
const start = parser.index;
|
|
||||||
let i = parser.index;
|
|
||||||
|
|
||||||
const code = full_char_code_at(parser.template, i);
|
const code = full_char_code_at(parser.template, i);
|
||||||
if (isIdentifierStart(code, true)) {
|
if (isIdentifierStart(code, true)) {
|
||||||
return {
|
return {
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
name: parser.read_identifier(),
|
name: parser.read_identifier(),
|
||||||
start,
|
start,
|
||||||
end: parser.index
|
end: parser.index,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_bracket_open(code)) {
|
if (!is_bracket_open(code)) {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'unexpected-token',
|
code: 'unexpected-token',
|
||||||
message: 'Expected identifier or destructure pattern'
|
message: 'Expected identifier or destructure pattern',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bracket_stack = [code];
|
const bracket_stack = [code];
|
||||||
i += code <= 0xffff ? 1 : 2;
|
i += code <= 0xffff ? 1 : 2;
|
||||||
|
|
||||||
while (i < parser.template.length) {
|
while (i < parser.template.length) {
|
||||||
const code = full_char_code_at(parser.template, i);
|
const code = full_char_code_at(parser.template, i);
|
||||||
if (is_bracket_open(code)) {
|
if (is_bracket_open(code)) {
|
||||||
bracket_stack.push(code);
|
bracket_stack.push(code);
|
||||||
} else if (is_bracket_close(code)) {
|
} else if (is_bracket_close(code)) {
|
||||||
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
|
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'unexpected-token',
|
code: 'unexpected-token',
|
||||||
message: `Expected ${String.fromCharCode(
|
message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`,
|
||||||
get_bracket_close(bracket_stack[bracket_stack.length - 1])
|
});
|
||||||
)}`
|
}
|
||||||
});
|
bracket_stack.pop();
|
||||||
}
|
if (bracket_stack.length === 0) {
|
||||||
bracket_stack.pop();
|
i += code <= 0xffff ? 1 : 2;
|
||||||
if (bracket_stack.length === 0) {
|
break;
|
||||||
i += code <= 0xffff ? 1 : 2;
|
}
|
||||||
break;
|
}
|
||||||
}
|
i += code <= 0xffff ? 1 : 2;
|
||||||
}
|
}
|
||||||
i += code <= 0xffff ? 1 : 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.index = i;
|
parser.index = i;
|
||||||
|
|
||||||
const pattern_string = parser.template.slice(start, i);
|
const pattern_string = parser.template.slice(start, i);
|
||||||
try {
|
try {
|
||||||
// the length of the `space_with_newline` has to be start - 1
|
// the length of the `space_with_newline` has to be start - 1
|
||||||
// because we added a `(` in front of the pattern_string,
|
// because we added a `(` in front of the pattern_string,
|
||||||
// which shifted the entire string to right by 1
|
// which shifted the entire string to right by 1
|
||||||
// so we offset it by removing 1 character in the `space_with_newline`
|
// so we offset it by removing 1 character in the `space_with_newline`
|
||||||
// to achieve that, we remove the 1st space encountered,
|
// to achieve that, we remove the 1st space encountered,
|
||||||
// so it will not affect the `column` of the node
|
// so it will not affect the `column` of the node
|
||||||
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
|
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
|
||||||
const first_space = space_with_newline.indexOf(' ');
|
const first_space = space_with_newline.indexOf(' ');
|
||||||
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
||||||
|
|
||||||
return (parse_expression_at(
|
return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left;
|
||||||
`${space_with_newline}(${pattern_string} = 1)`,
|
} catch (error) {
|
||||||
start - 1
|
parser.acorn_error(error);
|
||||||
) as any).left;
|
}
|
||||||
} catch (error) {
|
|
||||||
parser.acorn_error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,36 +6,39 @@ import { whitespace } from '../../utils/patterns.js';
|
||||||
// import { Node } from 'estree';
|
// import { Node } from 'estree';
|
||||||
|
|
||||||
export default function read_expression(parser: Parser): string {
|
export default function read_expression(parser: Parser): string {
|
||||||
try {
|
try {
|
||||||
const node = parse_expression_at(parser.template, parser.index);
|
const node = parse_expression_at(parser.template, parser.index);
|
||||||
|
|
||||||
let num_parens = 0;
|
let num_parens = 0;
|
||||||
|
|
||||||
for (let i = parser.index; i < node.start; i += 1) {
|
for (let i = parser.index; i < node.start; i += 1) {
|
||||||
if (parser.template[i] === '(') num_parens += 1;
|
if (parser.template[i] === '(') num_parens += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = node.end;
|
let index = node.end;
|
||||||
while (num_parens > 0) {
|
while (num_parens > 0) {
|
||||||
const char = parser.template[index];
|
const char = parser.template[index];
|
||||||
|
|
||||||
if (char === ')') {
|
if (char === ')') {
|
||||||
num_parens -= 1;
|
num_parens -= 1;
|
||||||
} else if (!whitespace.test(char)) {
|
} else if (!whitespace.test(char)) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'unexpected-token',
|
{
|
||||||
message: 'Expected )'
|
code: 'unexpected-token',
|
||||||
}, index);
|
message: 'Expected )',
|
||||||
}
|
},
|
||||||
|
index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.index = index;
|
parser.index = index;
|
||||||
|
|
||||||
return parser.template.substring(node.start, node.end);
|
return parser.template.substring(node.start, node.end);
|
||||||
// return node as Node;
|
// return node as Node;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
parser.acorn_error(err);
|
parser.acorn_error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,48 +8,53 @@ import { Node, Program } from 'estree';
|
||||||
const script_closing_tag = '</script>';
|
const script_closing_tag = '</script>';
|
||||||
|
|
||||||
function get_context(parser: Parser, attributes: any[], start: number): string {
|
function get_context(parser: Parser, attributes: any[], start: number): string {
|
||||||
const context = attributes.find(attribute => attribute.name === 'context');
|
const context = attributes.find((attribute) => attribute.name === 'context');
|
||||||
if (!context) return 'default';
|
if (!context) return 'default';
|
||||||
|
|
||||||
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
|
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-script',
|
{
|
||||||
message: 'context attribute must be static'
|
code: 'invalid-script',
|
||||||
}, start);
|
message: 'context attribute must be static',
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const value = context.value[0].data;
|
const value = context.value[0].data;
|
||||||
|
|
||||||
if (value !== 'module') {
|
if (value !== 'module') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-script',
|
{
|
||||||
message: 'If the context attribute is supplied, its value must be "module"'
|
code: 'invalid-script',
|
||||||
}, context.start);
|
message: 'If the context attribute is supplied, its value must be "module"',
|
||||||
}
|
},
|
||||||
|
context.start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
|
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
|
||||||
const script_start = parser.index;
|
const script_start = parser.index;
|
||||||
const script_end = parser.template.indexOf(script_closing_tag, script_start);
|
const script_end = parser.template.indexOf(script_closing_tag, script_start);
|
||||||
|
|
||||||
if (script_end === -1) {
|
if (script_end === -1) {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'unclosed-script',
|
code: 'unclosed-script',
|
||||||
message: '<script> must have a closing tag'
|
message: '<script> must have a closing tag',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
|
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + parser.template.slice(script_start, script_end);
|
||||||
parser.template.slice(script_start, script_end);
|
parser.index = script_end + script_closing_tag.length;
|
||||||
parser.index = script_end + script_closing_tag.length;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Script',
|
type: 'Script',
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
context: get_context(parser, attributes, start),
|
context: get_context(parser, attributes, start),
|
||||||
content: source
|
content: source,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import text from './text.js';
|
||||||
import { Parser } from '../index.js';
|
import { Parser } from '../index.js';
|
||||||
|
|
||||||
export default function fragment(parser: Parser) {
|
export default function fragment(parser: Parser) {
|
||||||
if (parser.match('<')) {
|
if (parser.match('<')) {
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser.match('{')) {
|
if (parser.match('{')) {
|
||||||
return mustache;
|
return mustache;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,403 +10,404 @@ import { TemplateNode } from '../../interfaces.js';
|
||||||
type TODO = any;
|
type TODO = any;
|
||||||
|
|
||||||
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
|
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
|
||||||
if (!block.children || block.children.length === 0) return; // AwaitBlock
|
if (!block.children || block.children.length === 0) return; // AwaitBlock
|
||||||
|
|
||||||
const first_child = block.children[0];
|
const first_child = block.children[0];
|
||||||
const last_child = block.children[block.children.length - 1];
|
const last_child = block.children[block.children.length - 1];
|
||||||
|
|
||||||
if (first_child.type === 'Text' && trim_before) {
|
if (first_child.type === 'Text' && trim_before) {
|
||||||
first_child.data = trim_start(first_child.data);
|
first_child.data = trim_start(first_child.data);
|
||||||
if (!first_child.data) block.children.shift();
|
if (!first_child.data) block.children.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last_child.type === 'Text' && trim_after) {
|
if (last_child.type === 'Text' && trim_after) {
|
||||||
last_child.data = trim_end(last_child.data);
|
last_child.data = trim_end(last_child.data);
|
||||||
if (!last_child.data) block.children.pop();
|
if (!last_child.data) block.children.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.else) {
|
if (block.else) {
|
||||||
trim_whitespace(block.else, trim_before, trim_after);
|
trim_whitespace(block.else, trim_before, trim_after);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (first_child.elseif) {
|
if (first_child.elseif) {
|
||||||
trim_whitespace(first_child, trim_before, trim_after);
|
trim_whitespace(first_child, trim_before, trim_after);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function mustache(parser: Parser) {
|
export default function mustache(parser: Parser) {
|
||||||
const start = parser.index;
|
const start = parser.index;
|
||||||
parser.index += 1;
|
parser.index += 1;
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
// {/if}, {/each}, {/await} or {/key}
|
// {/if}, {/each}, {/await} or {/key}
|
||||||
if (parser.eat('/')) {
|
if (parser.eat('/')) {
|
||||||
let block = parser.current();
|
let block = parser.current();
|
||||||
let expected: TODO;
|
let expected: TODO;
|
||||||
|
|
||||||
if (closing_tag_omitted(block.name)) {
|
if (closing_tag_omitted(block.name)) {
|
||||||
block.end = start;
|
block.end = start;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
block = parser.current();
|
block = parser.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
|
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
|
||||||
block.end = start;
|
block.end = start;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
block = parser.current();
|
block = parser.current();
|
||||||
|
|
||||||
expected = 'await';
|
expected = 'await';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === 'IfBlock') {
|
if (block.type === 'IfBlock') {
|
||||||
expected = 'if';
|
expected = 'if';
|
||||||
} else if (block.type === 'EachBlock') {
|
} else if (block.type === 'EachBlock') {
|
||||||
expected = 'each';
|
expected = 'each';
|
||||||
} else if (block.type === 'AwaitBlock') {
|
} else if (block.type === 'AwaitBlock') {
|
||||||
expected = 'await';
|
expected = 'await';
|
||||||
} else if (block.type === 'KeyBlock') {
|
} else if (block.type === 'KeyBlock') {
|
||||||
expected = 'key';
|
expected = 'key';
|
||||||
} else {
|
} else {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'unexpected-block-close',
|
code: 'unexpected-block-close',
|
||||||
message: 'Unexpected block closing tag'
|
message: 'Unexpected block closing tag',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.eat(expected, true);
|
parser.eat(expected, true);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
while (block.elseif) {
|
while (block.elseif) {
|
||||||
block.end = parser.index;
|
block.end = parser.index;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
block = parser.current();
|
block = parser.current();
|
||||||
|
|
||||||
if (block.else) {
|
if (block.else) {
|
||||||
block.else.end = start;
|
block.else.end = start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip leading/trailing whitespace as necessary
|
// strip leading/trailing whitespace as necessary
|
||||||
const char_before = parser.template[block.start - 1];
|
const char_before = parser.template[block.start - 1];
|
||||||
const char_after = parser.template[parser.index];
|
const char_after = parser.template[parser.index];
|
||||||
const trim_before = !char_before || whitespace.test(char_before);
|
const trim_before = !char_before || whitespace.test(char_before);
|
||||||
const trim_after = !char_after || whitespace.test(char_after);
|
const trim_after = !char_after || whitespace.test(char_after);
|
||||||
|
|
||||||
trim_whitespace(block, trim_before, trim_after);
|
trim_whitespace(block, trim_before, trim_after);
|
||||||
|
|
||||||
block.end = parser.index;
|
block.end = parser.index;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
} else if (parser.eat(':else')) {
|
} else if (parser.eat(':else')) {
|
||||||
if (parser.eat('if')) {
|
if (parser.eat('if')) {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'invalid-elseif',
|
code: 'invalid-elseif',
|
||||||
message: "'elseif' should be 'else if'"
|
message: "'elseif' should be 'else if'",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
// :else if
|
// :else if
|
||||||
if (parser.eat('if')) {
|
if (parser.eat('if')) {
|
||||||
const block = parser.current();
|
const block = parser.current();
|
||||||
if (block.type !== 'IfBlock') {
|
if (block.type !== 'IfBlock') {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'invalid-elseif-placement',
|
code: 'invalid-elseif-placement',
|
||||||
message: parser.stack.some(block => block.type === 'IfBlock')
|
message: parser.stack.some((block) => block.type === 'IfBlock')
|
||||||
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
|
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
|
||||||
: 'Cannot have an {:else if ...} block outside an {#if ...} block'
|
: 'Cannot have an {:else if ...} block outside an {#if ...} block',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
|
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
block.else = {
|
block.else = {
|
||||||
start: parser.index,
|
start: parser.index,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'ElseBlock',
|
type: 'ElseBlock',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
start: parser.index,
|
start: parser.index,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'IfBlock',
|
type: 'IfBlock',
|
||||||
elseif: true,
|
elseif: true,
|
||||||
expression,
|
expression,
|
||||||
children: []
|
children: [],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
parser.stack.push(block.else.children[0]);
|
parser.stack.push(block.else.children[0]);
|
||||||
} else {
|
} else {
|
||||||
// :else
|
// :else
|
||||||
const block = parser.current();
|
const block = parser.current();
|
||||||
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
|
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'invalid-else-placement',
|
code: 'invalid-else-placement',
|
||||||
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
|
message: parser.stack.some((block) => block.type === 'IfBlock' || block.type === 'EachBlock')
|
||||||
? `Expected to close ${to_string(block)} before seeing {:else} block`
|
? `Expected to close ${to_string(block)} before seeing {:else} block`
|
||||||
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block'
|
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
block.else = {
|
block.else = {
|
||||||
start: parser.index,
|
start: parser.index,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'ElseBlock',
|
type: 'ElseBlock',
|
||||||
children: []
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
parser.stack.push(block.else);
|
parser.stack.push(block.else);
|
||||||
}
|
}
|
||||||
} else if (parser.match(':then') || parser.match(':catch')) {
|
} else if (parser.match(':then') || parser.match(':catch')) {
|
||||||
const block = parser.current();
|
const block = parser.current();
|
||||||
const is_then = parser.eat(':then') || !parser.eat(':catch');
|
const is_then = parser.eat(':then') || !parser.eat(':catch');
|
||||||
|
|
||||||
if (is_then) {
|
if (is_then) {
|
||||||
if (block.type !== 'PendingBlock') {
|
if (block.type !== 'PendingBlock') {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'invalid-then-placement',
|
code: 'invalid-then-placement',
|
||||||
message: parser.stack.some(block => block.type === 'PendingBlock')
|
message: parser.stack.some((block) => block.type === 'PendingBlock')
|
||||||
? `Expected to close ${to_string(block)} before seeing {:then} block`
|
? `Expected to close ${to_string(block)} before seeing {:then} block`
|
||||||
: 'Cannot have an {:then} block outside an {#await ...} block'
|
: 'Cannot have an {:then} block outside an {#await ...} block',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
|
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'invalid-catch-placement',
|
code: 'invalid-catch-placement',
|
||||||
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
|
message: parser.stack.some((block) => block.type === 'ThenBlock' || block.type === 'PendingBlock')
|
||||||
? `Expected to close ${to_string(block)} before seeing {:catch} block`
|
? `Expected to close ${to_string(block)} before seeing {:catch} block`
|
||||||
: 'Cannot have an {:catch} block outside an {#await ...} block'
|
: 'Cannot have an {:catch} block outside an {#await ...} block',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
block.end = start;
|
block.end = start;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
const await_block = parser.current();
|
const await_block = parser.current();
|
||||||
|
|
||||||
if (!parser.eat('}')) {
|
if (!parser.eat('}')) {
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
await_block[is_then ? 'value' : 'error'] = read_context(parser);
|
await_block[is_then ? 'value' : 'error'] = read_context(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const new_block: TemplateNode = {
|
const new_block: TemplateNode = {
|
||||||
start,
|
start,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
end: null,
|
end: null,
|
||||||
type: is_then ? 'ThenBlock' : 'CatchBlock',
|
type: is_then ? 'ThenBlock' : 'CatchBlock',
|
||||||
children: [],
|
children: [],
|
||||||
skip: false
|
skip: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await_block[is_then ? 'then' : 'catch'] = new_block;
|
await_block[is_then ? 'then' : 'catch'] = new_block;
|
||||||
parser.stack.push(new_block);
|
parser.stack.push(new_block);
|
||||||
} else if (parser.eat('#')) {
|
} else if (parser.eat('#')) {
|
||||||
// {#if foo}, {#each foo} or {#await foo}
|
// {#if foo}, {#each foo} or {#await foo}
|
||||||
let type;
|
let type;
|
||||||
|
|
||||||
if (parser.eat('if')) {
|
if (parser.eat('if')) {
|
||||||
type = 'IfBlock';
|
type = 'IfBlock';
|
||||||
} else if (parser.eat('each')) {
|
} else if (parser.eat('each')) {
|
||||||
type = 'EachBlock';
|
type = 'EachBlock';
|
||||||
} else if (parser.eat('await')) {
|
} else if (parser.eat('await')) {
|
||||||
type = 'AwaitBlock';
|
type = 'AwaitBlock';
|
||||||
} else if (parser.eat('key')) {
|
} else if (parser.eat('key')) {
|
||||||
type = 'KeyBlock';
|
type = 'KeyBlock';
|
||||||
} else {
|
} else {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'expected-block-type',
|
code: 'expected-block-type',
|
||||||
message: 'Expected if, each, await or key'
|
message: 'Expected if, each, await or key',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
|
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const block: TemplateNode = type === 'AwaitBlock' ?
|
const block: TemplateNode =
|
||||||
{
|
type === 'AwaitBlock'
|
||||||
start,
|
? {
|
||||||
end: null,
|
start,
|
||||||
type,
|
end: null,
|
||||||
expression,
|
type,
|
||||||
value: null,
|
expression,
|
||||||
error: null,
|
value: null,
|
||||||
pending: {
|
error: null,
|
||||||
start: null,
|
pending: {
|
||||||
end: null,
|
start: null,
|
||||||
type: 'PendingBlock',
|
end: null,
|
||||||
children: [],
|
type: 'PendingBlock',
|
||||||
skip: true
|
children: [],
|
||||||
},
|
skip: true,
|
||||||
then: {
|
},
|
||||||
start: null,
|
then: {
|
||||||
end: null,
|
start: null,
|
||||||
type: 'ThenBlock',
|
end: null,
|
||||||
children: [],
|
type: 'ThenBlock',
|
||||||
skip: true
|
children: [],
|
||||||
},
|
skip: true,
|
||||||
catch: {
|
},
|
||||||
start: null,
|
catch: {
|
||||||
end: null,
|
start: null,
|
||||||
type: 'CatchBlock',
|
end: null,
|
||||||
children: [],
|
type: 'CatchBlock',
|
||||||
skip: true
|
children: [],
|
||||||
}
|
skip: true,
|
||||||
} :
|
},
|
||||||
{
|
}
|
||||||
start,
|
: {
|
||||||
end: null,
|
start,
|
||||||
type,
|
end: null,
|
||||||
expression,
|
type,
|
||||||
children: []
|
expression,
|
||||||
};
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
// {#each} blocks must declare a context – {#each list as item}
|
// {#each} blocks must declare a context – {#each list as item}
|
||||||
if (type === 'EachBlock') {
|
if (type === 'EachBlock') {
|
||||||
parser.eat('as', true);
|
parser.eat('as', true);
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
|
|
||||||
block.context = read_context(parser);
|
block.context = read_context(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
if (parser.eat(',')) {
|
if (parser.eat(',')) {
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
block.index = parser.read_identifier();
|
block.index = parser.read_identifier();
|
||||||
if (!block.index) {
|
if (!block.index) {
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'expected-name',
|
code: 'expected-name',
|
||||||
message: 'Expected name'
|
message: 'Expected name',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser.eat('(')) {
|
if (parser.eat('(')) {
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
block.key = read_expression(parser);
|
block.key = read_expression(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat(')', true);
|
parser.eat(')', true);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
|
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
|
||||||
if (await_block_shorthand) {
|
if (await_block_shorthand) {
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
block.value = read_context(parser);
|
block.value = read_context(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
|
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
|
||||||
if (await_block_catch_shorthand) {
|
if (await_block_catch_shorthand) {
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
block.error = read_context(parser);
|
block.error = read_context(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
parser.current().children.push(block);
|
parser.current().children.push(block);
|
||||||
parser.stack.push(block);
|
parser.stack.push(block);
|
||||||
|
|
||||||
if (type === 'AwaitBlock') {
|
if (type === 'AwaitBlock') {
|
||||||
let child_block;
|
let child_block;
|
||||||
if (await_block_shorthand) {
|
if (await_block_shorthand) {
|
||||||
block.then.skip = false;
|
block.then.skip = false;
|
||||||
child_block = block.then;
|
child_block = block.then;
|
||||||
} else if (await_block_catch_shorthand) {
|
} else if (await_block_catch_shorthand) {
|
||||||
block.catch.skip = false;
|
block.catch.skip = false;
|
||||||
child_block = block.catch;
|
child_block = block.catch;
|
||||||
} else {
|
} else {
|
||||||
block.pending.skip = false;
|
block.pending.skip = false;
|
||||||
child_block = block.pending;
|
child_block = block.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
child_block.start = parser.index;
|
child_block.start = parser.index;
|
||||||
parser.stack.push(child_block);
|
parser.stack.push(child_block);
|
||||||
}
|
}
|
||||||
} else if (parser.eat('@html')) {
|
} else if (parser.eat('@html')) {
|
||||||
// {@html content} tag
|
// {@html content} tag
|
||||||
parser.require_whitespace();
|
parser.require_whitespace();
|
||||||
|
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
parser.current().children.push({
|
parser.current().children.push({
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'RawMustacheTag',
|
type: 'RawMustacheTag',
|
||||||
expression
|
expression,
|
||||||
});
|
});
|
||||||
} else if (parser.eat('@debug')) {
|
} else if (parser.eat('@debug')) {
|
||||||
// let identifiers;
|
// let identifiers;
|
||||||
|
|
||||||
// // Implies {@debug} which indicates "debug all"
|
// // Implies {@debug} which indicates "debug all"
|
||||||
// if (parser.read(/\s*}/)) {
|
// if (parser.read(/\s*}/)) {
|
||||||
// identifiers = [];
|
// identifiers = [];
|
||||||
// } else {
|
// } else {
|
||||||
// const expression = read_expression(parser);
|
// const expression = read_expression(parser);
|
||||||
|
|
||||||
// identifiers = expression.type === 'SequenceExpression'
|
// identifiers = expression.type === 'SequenceExpression'
|
||||||
// ? expression.expressions
|
// ? expression.expressions
|
||||||
// : [expression];
|
// : [expression];
|
||||||
|
|
||||||
// identifiers.forEach(node => {
|
// identifiers.forEach(node => {
|
||||||
// if (node.type !== 'Identifier') {
|
// if (node.type !== 'Identifier') {
|
||||||
// parser.error({
|
// parser.error({
|
||||||
// code: 'invalid-debug-args',
|
// code: 'invalid-debug-args',
|
||||||
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
|
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
|
||||||
// }, node.start);
|
// }, node.start);
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// parser.allow_whitespace();
|
// parser.allow_whitespace();
|
||||||
// parser.eat('}', true);
|
// parser.eat('}', true);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// parser.current().children.push({
|
// parser.current().children.push({
|
||||||
// start,
|
// start,
|
||||||
// end: parser.index,
|
// end: parser.index,
|
||||||
// type: 'DebugTag',
|
// type: 'DebugTag',
|
||||||
// identifiers
|
// identifiers
|
||||||
// });
|
// });
|
||||||
throw new Error('@debug not yet supported');
|
throw new Error('@debug not yet supported');
|
||||||
} else {
|
} else {
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
parser.current().children.push({
|
parser.current().children.push({
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'MustacheTag',
|
type: 'MustacheTag',
|
||||||
expression
|
expression,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,29 +14,29 @@ import list from '../../utils/list.js';
|
||||||
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
|
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
|
||||||
|
|
||||||
const meta_tags = new Map([
|
const meta_tags = new Map([
|
||||||
['svelte:head', 'Head'],
|
['svelte:head', 'Head'],
|
||||||
['svelte:options', 'Options'],
|
['svelte:options', 'Options'],
|
||||||
['svelte:window', 'Window'],
|
['svelte:window', 'Window'],
|
||||||
['svelte:body', 'Body']
|
['svelte:body', 'Body'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
|
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
|
||||||
|
|
||||||
const specials = new Map([
|
const specials = new Map([
|
||||||
[
|
[
|
||||||
'script',
|
'script',
|
||||||
{
|
{
|
||||||
read: read_script,
|
read: read_script,
|
||||||
property: 'js'
|
property: 'js',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'style',
|
'style',
|
||||||
{
|
{
|
||||||
read: read_style,
|
read: read_style,
|
||||||
property: 'css'
|
property: 'css',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SELF = /^svelte:self(?=[\s/>])/;
|
const SELF = /^svelte:self(?=[\s/>])/;
|
||||||
|
@ -44,489 +44,531 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
|
||||||
const SLOT = /^svelte:fragment(?=[\s/>])/;
|
const SLOT = /^svelte:fragment(?=[\s/>])/;
|
||||||
|
|
||||||
function parent_is_head(stack) {
|
function parent_is_head(stack) {
|
||||||
let i = stack.length;
|
let i = stack.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
const { type } = stack[i];
|
const { type } = stack[i];
|
||||||
if (type === 'Head') return true;
|
if (type === 'Head') return true;
|
||||||
if (type === 'Element' || type === 'InlineComponent') return false;
|
if (type === 'Element' || type === 'InlineComponent') return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function tag(parser: Parser) {
|
export default function tag(parser: Parser) {
|
||||||
const start = parser.index++;
|
const start = parser.index++;
|
||||||
|
|
||||||
let parent = parser.current();
|
let parent = parser.current();
|
||||||
|
|
||||||
if (parser.eat('!--')) {
|
if (parser.eat('!--')) {
|
||||||
const data = parser.read_until(/-->/);
|
const data = parser.read_until(/-->/);
|
||||||
parser.eat('-->', true, 'comment was left open, expected -->');
|
parser.eat('-->', true, 'comment was left open, expected -->');
|
||||||
|
|
||||||
parser.current().children.push({
|
parser.current().children.push({
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'Comment',
|
type: 'Comment',
|
||||||
data
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const is_closing_tag = parser.eat('/');
|
const is_closing_tag = parser.eat('/');
|
||||||
|
|
||||||
const name = read_tag_name(parser);
|
const name = read_tag_name(parser);
|
||||||
|
|
||||||
if (meta_tags.has(name)) {
|
if (meta_tags.has(name)) {
|
||||||
const slug = meta_tags.get(name).toLowerCase();
|
const slug = meta_tags.get(name).toLowerCase();
|
||||||
if (is_closing_tag) {
|
if (is_closing_tag) {
|
||||||
if (
|
if ((name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length) {
|
||||||
(name === 'svelte:window' || name === 'svelte:body') &&
|
parser.error(
|
||||||
parser.current().children.length
|
{
|
||||||
) {
|
code: `invalid-${slug}-content`,
|
||||||
parser.error({
|
message: `<${name}> cannot have children`,
|
||||||
code: `invalid-${slug}-content`,
|
},
|
||||||
message: `<${name}> cannot have children`
|
parser.current().children[0].start
|
||||||
}, parser.current().children[0].start);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (name in parser.meta_tags) {
|
if (name in parser.meta_tags) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: `duplicate-${slug}`,
|
{
|
||||||
message: `A component can only have one <${name}> tag`
|
code: `duplicate-${slug}`,
|
||||||
}, start);
|
message: `A component can only have one <${name}> tag`,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (parser.stack.length > 1) {
|
if (parser.stack.length > 1) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: `invalid-${slug}-placement`,
|
{
|
||||||
message: `<${name}> tags cannot be inside elements or blocks`
|
code: `invalid-${slug}-placement`,
|
||||||
}, start);
|
message: `<${name}> tags cannot be inside elements or blocks`,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
parser.meta_tags[name] = true;
|
parser.meta_tags[name] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = meta_tags.has(name)
|
const type = meta_tags.has(name)
|
||||||
? meta_tags.get(name)
|
? meta_tags.get(name)
|
||||||
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
|
: /[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
|
||||||
: name === 'svelte:fragment' ? 'SlotTemplate'
|
? 'InlineComponent'
|
||||||
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
|
: name === 'svelte:fragment'
|
||||||
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
|
? 'SlotTemplate'
|
||||||
|
: name === 'title' && parent_is_head(parser.stack)
|
||||||
|
? 'Title'
|
||||||
|
: name === 'slot' && !parser.customElement
|
||||||
|
? 'Slot'
|
||||||
|
: 'Element';
|
||||||
|
|
||||||
const element: TemplateNode = {
|
const element: TemplateNode = {
|
||||||
start,
|
start,
|
||||||
end: null, // filled in later
|
end: null, // filled in later
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
children: []
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
if (is_closing_tag) {
|
if (is_closing_tag) {
|
||||||
if (is_void(name)) {
|
if (is_void(name)) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-void-content',
|
{
|
||||||
message: `<${name}> is a void element and cannot have children, or a closing tag`
|
code: 'invalid-void-content',
|
||||||
}, start);
|
message: `<${name}> is a void element and cannot have children, or a closing tag`,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
parser.eat('>', true);
|
parser.eat('>', true);
|
||||||
|
|
||||||
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
||||||
while (parent.name !== name) {
|
while (parent.name !== name) {
|
||||||
if (parent.type !== 'Element') {
|
if (parent.type !== 'Element') {
|
||||||
const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
|
const message =
|
||||||
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
|
parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
|
||||||
: `</${name}> attempted to close an element that was not open`;
|
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
|
||||||
parser.error({
|
: `</${name}> attempted to close an element that was not open`;
|
||||||
code: 'invalid-closing-tag',
|
parser.error(
|
||||||
message
|
{
|
||||||
}, start);
|
code: 'invalid-closing-tag',
|
||||||
}
|
message,
|
||||||
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
parent.end = start;
|
parent.end = start;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
|
|
||||||
parent = parser.current();
|
parent = parser.current();
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.end = parser.index;
|
parent.end = parser.index;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
|
|
||||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||||
parser.last_auto_closed_tag = null;
|
parser.last_auto_closed_tag = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (closing_tag_omitted(parent.name, name)) {
|
} else if (closing_tag_omitted(parent.name, name)) {
|
||||||
parent.end = start;
|
parent.end = start;
|
||||||
parser.stack.pop();
|
parser.stack.pop();
|
||||||
parser.last_auto_closed_tag = {
|
parser.last_auto_closed_tag = {
|
||||||
tag: parent.name,
|
tag: parent.name,
|
||||||
reason: name,
|
reason: name,
|
||||||
depth: parser.stack.length
|
depth: parser.stack.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const unique_names: Set<string> = new Set();
|
const unique_names: Set<string> = new Set();
|
||||||
|
|
||||||
let attribute;
|
let attribute;
|
||||||
while ((attribute = read_attribute(parser, unique_names))) {
|
while ((attribute = read_attribute(parser, unique_names))) {
|
||||||
element.attributes.push(attribute);
|
element.attributes.push(attribute);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'svelte:component') {
|
if (name === 'svelte:component') {
|
||||||
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
|
const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
|
||||||
if (!~index) {
|
if (!~index) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'missing-component-definition',
|
{
|
||||||
message: "<svelte:component> must have a 'this' attribute"
|
code: 'missing-component-definition',
|
||||||
}, start);
|
message: "<svelte:component> must have a 'this' attribute",
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const definition = element.attributes.splice(index, 1)[0];
|
const definition = element.attributes.splice(index, 1)[0];
|
||||||
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
|
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-component-definition',
|
{
|
||||||
message: 'invalid component definition'
|
code: 'invalid-component-definition',
|
||||||
}, definition.start);
|
message: 'invalid component definition',
|
||||||
}
|
},
|
||||||
|
definition.start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
element.expression = definition.value[0].expression;
|
element.expression = definition.value[0].expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
// special cases – top-level <script> and <style>
|
// special cases – top-level <script> and <style>
|
||||||
if (specials.has(name) && parser.stack.length === 1) {
|
if (specials.has(name) && parser.stack.length === 1) {
|
||||||
const special = specials.get(name);
|
const special = specials.get(name);
|
||||||
|
|
||||||
parser.eat('>', true);
|
parser.eat('>', true);
|
||||||
const content = special.read(parser, start, element.attributes);
|
const content = special.read(parser, start, element.attributes);
|
||||||
if (content) parser[special.property].push(content);
|
if (content) parser[special.property].push(content);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.current().children.push(element);
|
parser.current().children.push(element);
|
||||||
|
|
||||||
const self_closing = parser.eat('/') || is_void(name);
|
const self_closing = parser.eat('/') || is_void(name);
|
||||||
|
|
||||||
parser.eat('>', true);
|
parser.eat('>', true);
|
||||||
|
|
||||||
if (self_closing) {
|
if (self_closing) {
|
||||||
// don't push self-closing elements onto the stack
|
// don't push self-closing elements onto the stack
|
||||||
element.end = parser.index;
|
element.end = parser.index;
|
||||||
} else if (name === 'textarea') {
|
} else if (name === 'textarea') {
|
||||||
// special case
|
// special case
|
||||||
element.children = read_sequence(
|
element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>');
|
||||||
parser,
|
parser.read(/<\/textarea>/);
|
||||||
() =>
|
element.end = parser.index;
|
||||||
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
|
} else if (name === 'script' || name === 'style') {
|
||||||
);
|
// special case
|
||||||
parser.read(/<\/textarea>/);
|
const start = parser.index;
|
||||||
element.end = parser.index;
|
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||||
} else if (name === 'script' || name === 'style') {
|
const end = parser.index;
|
||||||
// special case
|
element.children.push({ start, end, type: 'Text', data });
|
||||||
const start = parser.index;
|
parser.eat(`</${name}>`, true);
|
||||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
element.end = parser.index;
|
||||||
const end = parser.index;
|
} else {
|
||||||
element.children.push({ start, end, type: 'Text', data });
|
parser.stack.push(element);
|
||||||
parser.eat(`</${name}>`, true);
|
}
|
||||||
element.end = parser.index;
|
|
||||||
} else {
|
|
||||||
parser.stack.push(element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_tag_name(parser: Parser) {
|
function read_tag_name(parser: Parser) {
|
||||||
const start = parser.index;
|
const start = parser.index;
|
||||||
|
|
||||||
if (parser.read(SELF)) {
|
if (parser.read(SELF)) {
|
||||||
// check we're inside a block, otherwise this
|
// check we're inside a block, otherwise this
|
||||||
// will cause infinite recursion
|
// will cause infinite recursion
|
||||||
let i = parser.stack.length;
|
let i = parser.stack.length;
|
||||||
let legal = false;
|
let legal = false;
|
||||||
|
|
||||||
while (i--) {
|
while (i--) {
|
||||||
const fragment = parser.stack[i];
|
const fragment = parser.stack[i];
|
||||||
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
|
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
|
||||||
legal = true;
|
legal = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!legal) {
|
if (!legal) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-self-placement',
|
{
|
||||||
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
|
code: 'invalid-self-placement',
|
||||||
}, start);
|
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return 'svelte:self';
|
return 'svelte:self';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser.read(COMPONENT)) return 'svelte:component';
|
if (parser.read(COMPONENT)) return 'svelte:component';
|
||||||
|
|
||||||
if (parser.read(SLOT)) return 'svelte:fragment';
|
if (parser.read(SLOT)) return 'svelte:fragment';
|
||||||
|
|
||||||
const name = parser.read_until(/(\s|\/|>)/);
|
const name = parser.read_until(/(\s|\/|>)/);
|
||||||
|
|
||||||
if (meta_tags.has(name)) return name;
|
if (meta_tags.has(name)) return name;
|
||||||
|
|
||||||
if (name.startsWith('svelte:')) {
|
if (name.startsWith('svelte:')) {
|
||||||
const match = fuzzymatch(name.slice(7), valid_meta_tags);
|
const match = fuzzymatch(name.slice(7), valid_meta_tags);
|
||||||
|
|
||||||
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
|
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
|
||||||
if (match) message += ` (did you mean '${match}'?)`;
|
if (match) message += ` (did you mean '${match}'?)`;
|
||||||
|
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-tag-name',
|
{
|
||||||
message
|
code: 'invalid-tag-name',
|
||||||
}, start);
|
message,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid_tag_name.test(name)) {
|
if (!valid_tag_name.test(name)) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-tag-name',
|
{
|
||||||
message: 'Expected valid tag name'
|
code: 'invalid-tag-name',
|
||||||
}, start);
|
message: 'Expected valid tag name',
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_attribute(parser: Parser, unique_names: Set<string>) {
|
function read_attribute(parser: Parser, unique_names: Set<string>) {
|
||||||
const start = parser.index;
|
const start = parser.index;
|
||||||
|
|
||||||
function check_unique(name: string) {
|
function check_unique(name: string) {
|
||||||
if (unique_names.has(name)) {
|
if (unique_names.has(name)) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'duplicate-attribute',
|
{
|
||||||
message: 'Attributes need to be unique'
|
code: 'duplicate-attribute',
|
||||||
}, start);
|
message: 'Attributes need to be unique',
|
||||||
}
|
},
|
||||||
unique_names.add(name);
|
start
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
unique_names.add(name);
|
||||||
|
}
|
||||||
|
|
||||||
if (parser.eat('{')) {
|
if (parser.eat('{')) {
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
if (parser.eat('...')) {
|
if (parser.eat('...')) {
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'Spread',
|
type: 'Spread',
|
||||||
expression
|
expression,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const value_start = parser.index;
|
const value_start = parser.index;
|
||||||
|
|
||||||
const name = parser.read_identifier();
|
const name = parser.read_identifier();
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
check_unique(name);
|
check_unique(name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'Attribute',
|
type: 'Attribute',
|
||||||
name,
|
name,
|
||||||
value: [{
|
value: [
|
||||||
start: value_start,
|
{
|
||||||
end: value_start + name.length,
|
start: value_start,
|
||||||
type: 'AttributeShorthand',
|
end: value_start + name.length,
|
||||||
expression: {
|
type: 'AttributeShorthand',
|
||||||
start: value_start,
|
expression: {
|
||||||
end: value_start + name.length,
|
start: value_start,
|
||||||
type: 'Identifier',
|
end: value_start + name.length,
|
||||||
name
|
type: 'Identifier',
|
||||||
}
|
name,
|
||||||
}]
|
},
|
||||||
};
|
},
|
||||||
}
|
],
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
const name = parser.read_until(/[\s=\/>"']/);
|
const name = parser.read_until(/[\s=\/>"']/);
|
||||||
if (!name) return null;
|
if (!name) return null;
|
||||||
|
|
||||||
let end = parser.index;
|
let end = parser.index;
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
|
|
||||||
const colon_index = name.indexOf(':');
|
const colon_index = name.indexOf(':');
|
||||||
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
||||||
|
|
||||||
let value: any[] | true = true;
|
let value: any[] | true = true;
|
||||||
if (parser.eat('=')) {
|
if (parser.eat('=')) {
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
value = read_attribute_value(parser);
|
value = read_attribute_value(parser);
|
||||||
end = parser.index;
|
end = parser.index;
|
||||||
} else if (parser.match_regex(/["']/)) {
|
} else if (parser.match_regex(/["']/)) {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'unexpected-token',
|
{
|
||||||
message: 'Expected ='
|
code: 'unexpected-token',
|
||||||
}, parser.index);
|
message: 'Expected =',
|
||||||
}
|
},
|
||||||
|
parser.index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type) {
|
if (type) {
|
||||||
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
||||||
|
|
||||||
if (type === 'Binding' && directive_name !== 'this') {
|
if (type === 'Binding' && directive_name !== 'this') {
|
||||||
check_unique(directive_name);
|
check_unique(directive_name);
|
||||||
} else if (type !== 'EventHandler' && type !== 'Action') {
|
} else if (type !== 'EventHandler' && type !== 'Action') {
|
||||||
check_unique(name);
|
check_unique(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'Ref') {
|
if (type === 'Ref') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-ref-directive',
|
{
|
||||||
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
|
code: 'invalid-ref-directive',
|
||||||
}, start);
|
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
|
||||||
}
|
},
|
||||||
|
start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'Class' && directive_name === '') {
|
if (type === 'Class' && directive_name === '') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-class-directive',
|
{
|
||||||
message: 'Class binding name cannot be empty'
|
code: 'invalid-class-directive',
|
||||||
}, start + colon_index + 1);
|
message: 'Class binding name cannot be empty',
|
||||||
}
|
},
|
||||||
|
start + colon_index + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (value[0]) {
|
if (value[0]) {
|
||||||
if ((value as any[]).length > 1 || value[0].type === 'Text') {
|
if ((value as any[]).length > 1 || value[0].type === 'Text') {
|
||||||
parser.error({
|
parser.error(
|
||||||
code: 'invalid-directive-value',
|
{
|
||||||
message: 'Directive value must be a JavaScript expression enclosed in curly braces'
|
code: 'invalid-directive-value',
|
||||||
}, value[0].start);
|
message: 'Directive value must be a JavaScript expression enclosed in curly braces',
|
||||||
}
|
},
|
||||||
}
|
value[0].start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const directive: Directive = {
|
const directive: Directive = {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
type,
|
type,
|
||||||
name: directive_name,
|
name: directive_name,
|
||||||
modifiers,
|
modifiers,
|
||||||
expression: (value[0] && value[0].expression) || null
|
expression: (value[0] && value[0].expression) || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 'Transition') {
|
if (type === 'Transition') {
|
||||||
const direction = name.slice(0, colon_index);
|
const direction = name.slice(0, colon_index);
|
||||||
directive.intro = direction === 'in' || direction === 'transition';
|
directive.intro = direction === 'in' || direction === 'transition';
|
||||||
directive.outro = direction === 'out' || direction === 'transition';
|
directive.outro = direction === 'out' || direction === 'transition';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
|
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
|
||||||
directive.expression = {
|
directive.expression = {
|
||||||
start: directive.start + colon_index + 1,
|
start: directive.start + colon_index + 1,
|
||||||
end: directive.end,
|
end: directive.end,
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
name: directive.name
|
name: directive.name,
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
return directive;
|
return directive;
|
||||||
}
|
}
|
||||||
|
|
||||||
check_unique(name);
|
check_unique(name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
type: 'Attribute',
|
type: 'Attribute',
|
||||||
name,
|
name,
|
||||||
value
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_directive_type(name: string): DirectiveType {
|
function get_directive_type(name: string): DirectiveType {
|
||||||
if (name === 'use') return 'Action';
|
if (name === 'use') return 'Action';
|
||||||
if (name === 'animate') return 'Animation';
|
if (name === 'animate') return 'Animation';
|
||||||
if (name === 'bind') return 'Binding';
|
if (name === 'bind') return 'Binding';
|
||||||
if (name === 'class') return 'Class';
|
if (name === 'class') return 'Class';
|
||||||
if (name === 'on') return 'EventHandler';
|
if (name === 'on') return 'EventHandler';
|
||||||
if (name === 'let') return 'Let';
|
if (name === 'let') return 'Let';
|
||||||
if (name === 'ref') return 'Ref';
|
if (name === 'ref') return 'Ref';
|
||||||
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
|
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_attribute_value(parser: Parser) {
|
function read_attribute_value(parser: Parser) {
|
||||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||||
|
|
||||||
const regex = (
|
const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/;
|
||||||
quote_mark === "'" ? /'/ :
|
|
||||||
quote_mark === '"' ? /"/ :
|
|
||||||
/(\/>|[\s"'=<>`])/
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = read_sequence(parser, () => !!parser.match_regex(regex));
|
const value = read_sequence(parser, () => !!parser.match_regex(regex));
|
||||||
|
|
||||||
if (quote_mark) parser.index += 1;
|
if (quote_mark) parser.index += 1;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
||||||
let current_chunk: Text = {
|
let current_chunk: Text = {
|
||||||
start: parser.index,
|
start: parser.index,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
raw: '',
|
raw: '',
|
||||||
data: null
|
data: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function flush() {
|
function flush() {
|
||||||
if (current_chunk.raw) {
|
if (current_chunk.raw) {
|
||||||
current_chunk.data = decode_character_references(current_chunk.raw);
|
current_chunk.data = decode_character_references(current_chunk.raw);
|
||||||
current_chunk.end = parser.index;
|
current_chunk.end = parser.index;
|
||||||
chunks.push(current_chunk);
|
chunks.push(current_chunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks: TemplateNode[] = [];
|
const chunks: TemplateNode[] = [];
|
||||||
|
|
||||||
while (parser.index < parser.template.length) {
|
while (parser.index < parser.template.length) {
|
||||||
const index = parser.index;
|
const index = parser.index;
|
||||||
|
|
||||||
if (done()) {
|
if (done()) {
|
||||||
flush();
|
flush();
|
||||||
return chunks;
|
return chunks;
|
||||||
} else if (parser.eat('{')) {
|
} else if (parser.eat('{')) {
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
const expression = read_expression(parser);
|
const expression = read_expression(parser);
|
||||||
parser.allow_whitespace();
|
parser.allow_whitespace();
|
||||||
parser.eat('}', true);
|
parser.eat('}', true);
|
||||||
|
|
||||||
chunks.push({
|
chunks.push({
|
||||||
start: index,
|
start: index,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'MustacheTag',
|
type: 'MustacheTag',
|
||||||
expression
|
expression,
|
||||||
});
|
});
|
||||||
|
|
||||||
current_chunk = {
|
current_chunk = {
|
||||||
start: parser.index,
|
start: parser.index,
|
||||||
end: null,
|
end: null,
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
raw: '',
|
raw: '',
|
||||||
data: null
|
data: null,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
current_chunk.raw += parser.template[parser.index++];
|
current_chunk.raw += parser.template[parser.index++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.error({
|
parser.error({
|
||||||
code: 'unexpected-eof',
|
code: 'unexpected-eof',
|
||||||
message: 'Unexpected end of input'
|
message: 'Unexpected end of input',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,25 +4,21 @@ import { decode_character_references } from '../utils/html.js';
|
||||||
import { Parser } from '../index.js';
|
import { Parser } from '../index.js';
|
||||||
|
|
||||||
export default function text(parser: Parser) {
|
export default function text(parser: Parser) {
|
||||||
const start = parser.index;
|
const start = parser.index;
|
||||||
|
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
while (
|
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
|
||||||
parser.index < parser.template.length &&
|
data += parser.template[parser.index++];
|
||||||
!parser.match('<') &&
|
}
|
||||||
!parser.match('{')
|
|
||||||
) {
|
|
||||||
data += parser.template[parser.index++];
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
start,
|
start,
|
||||||
end: parser.index,
|
end: parser.index,
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
raw: data,
|
raw: data,
|
||||||
data: decode_character_references(data)
|
data: decode_character_references(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
parser.current().children.push(node);
|
parser.current().children.push(node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,25 +6,22 @@ const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
|
||||||
const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0);
|
const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0);
|
||||||
|
|
||||||
export function is_bracket_open(code) {
|
export function is_bracket_open(code) {
|
||||||
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
|
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function is_bracket_close(code) {
|
export function is_bracket_close(code) {
|
||||||
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
|
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function is_bracket_pair(open, close) {
|
export function is_bracket_pair(open, close) {
|
||||||
return (
|
return (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE);
|
||||||
(open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) ||
|
|
||||||
(open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_bracket_close(open) {
|
export function get_bracket_close(open) {
|
||||||
if (open === SQUARE_BRACKET_OPEN) {
|
if (open === SQUARE_BRACKET_OPEN) {
|
||||||
return SQUARE_BRACKET_CLOSE;
|
return SQUARE_BRACKET_CLOSE;
|
||||||
}
|
}
|
||||||
if (open === CURLY_BRACKET_OPEN) {
|
if (open === CURLY_BRACKET_OPEN) {
|
||||||
return CURLY_BRACKET_CLOSE;
|
return CURLY_BRACKET_CLOSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,64 +3,61 @@
|
||||||
import entities from './entities.js';
|
import entities from './entities.js';
|
||||||
|
|
||||||
const windows_1252 = [
|
const windows_1252 = [
|
||||||
8364,
|
8364,
|
||||||
129,
|
129,
|
||||||
8218,
|
8218,
|
||||||
402,
|
402,
|
||||||
8222,
|
8222,
|
||||||
8230,
|
8230,
|
||||||
8224,
|
8224,
|
||||||
8225,
|
8225,
|
||||||
710,
|
710,
|
||||||
8240,
|
8240,
|
||||||
352,
|
352,
|
||||||
8249,
|
8249,
|
||||||
338,
|
338,
|
||||||
141,
|
141,
|
||||||
381,
|
381,
|
||||||
143,
|
143,
|
||||||
144,
|
144,
|
||||||
8216,
|
8216,
|
||||||
8217,
|
8217,
|
||||||
8220,
|
8220,
|
||||||
8221,
|
8221,
|
||||||
8226,
|
8226,
|
||||||
8211,
|
8211,
|
||||||
8212,
|
8212,
|
||||||
732,
|
732,
|
||||||
8482,
|
8482,
|
||||||
353,
|
353,
|
||||||
8250,
|
8250,
|
||||||
339,
|
339,
|
||||||
157,
|
157,
|
||||||
382,
|
382,
|
||||||
376
|
376,
|
||||||
];
|
];
|
||||||
|
|
||||||
const entity_pattern = new RegExp(
|
const entity_pattern = new RegExp(`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, 'g');
|
||||||
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
|
|
||||||
'g'
|
|
||||||
);
|
|
||||||
|
|
||||||
export function decode_character_references(html: string) {
|
export function decode_character_references(html: string) {
|
||||||
return html.replace(entity_pattern, (match, entity) => {
|
return html.replace(entity_pattern, (match, entity) => {
|
||||||
let code;
|
let code;
|
||||||
|
|
||||||
// Handle named entities
|
// Handle named entities
|
||||||
if (entity[0] !== '#') {
|
if (entity[0] !== '#') {
|
||||||
code = entities[entity];
|
code = entities[entity];
|
||||||
} else if (entity[1] === 'x') {
|
} else if (entity[1] === 'x') {
|
||||||
code = parseInt(entity.substring(2), 16);
|
code = parseInt(entity.substring(2), 16);
|
||||||
} else {
|
} else {
|
||||||
code = parseInt(entity.substring(1), 10);
|
code = parseInt(entity.substring(1), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
return String.fromCodePoint(validate_code(code));
|
return String.fromCodePoint(validate_code(code));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const NUL = 0;
|
const NUL = 0;
|
||||||
|
@ -71,83 +68,76 @@ const NUL = 0;
|
||||||
//
|
//
|
||||||
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
||||||
function validate_code(code: number) {
|
function validate_code(code: number) {
|
||||||
// line feed becomes generic whitespace
|
// line feed becomes generic whitespace
|
||||||
if (code === 10) {
|
if (code === 10) {
|
||||||
return 32;
|
return 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||||
if (code < 128) {
|
if (code < 128) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
|
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
|
||||||
// to correct the mistake or we'll end up with missing € signs and so on
|
// to correct the mistake or we'll end up with missing € signs and so on
|
||||||
if (code <= 159) {
|
if (code <= 159) {
|
||||||
return windows_1252[code - 128];
|
return windows_1252[code - 128];
|
||||||
}
|
}
|
||||||
|
|
||||||
// basic multilingual plane
|
// basic multilingual plane
|
||||||
if (code < 55296) {
|
if (code < 55296) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTF-16 surrogate halves
|
// UTF-16 surrogate halves
|
||||||
if (code <= 57343) {
|
if (code <= 57343) {
|
||||||
return NUL;
|
return NUL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rest of the basic multilingual plane
|
// rest of the basic multilingual plane
|
||||||
if (code <= 65535) {
|
if (code <= 65535) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||||
if (code >= 65536 && code <= 131071) {
|
if (code >= 65536 && code <= 131071) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||||
if (code >= 131072 && code <= 196607) {
|
if (code >= 131072 && code <= 196607) {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NUL;
|
return NUL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
|
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
|
||||||
const disallowed_contents = new Map([
|
const disallowed_contents = new Map([
|
||||||
['li', new Set(['li'])],
|
['li', new Set(['li'])],
|
||||||
['dt', new Set(['dt', 'dd'])],
|
['dt', new Set(['dt', 'dd'])],
|
||||||
['dd', new Set(['dt', 'dd'])],
|
['dd', new Set(['dt', 'dd'])],
|
||||||
[
|
['p', new Set('address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(' '))],
|
||||||
'p',
|
['rt', new Set(['rt', 'rp'])],
|
||||||
new Set(
|
['rp', new Set(['rt', 'rp'])],
|
||||||
'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
|
['optgroup', new Set(['optgroup'])],
|
||||||
' '
|
['option', new Set(['option', 'optgroup'])],
|
||||||
)
|
['thead', new Set(['tbody', 'tfoot'])],
|
||||||
)
|
['tbody', new Set(['tbody', 'tfoot'])],
|
||||||
],
|
['tfoot', new Set(['tbody'])],
|
||||||
['rt', new Set(['rt', 'rp'])],
|
['tr', new Set(['tr', 'tbody'])],
|
||||||
['rp', new Set(['rt', 'rp'])],
|
['td', new Set(['td', 'th', 'tr'])],
|
||||||
['optgroup', new Set(['optgroup'])],
|
['th', new Set(['td', 'th', 'tr'])],
|
||||||
['option', new Set(['option', 'optgroup'])],
|
|
||||||
['thead', new Set(['tbody', 'tfoot'])],
|
|
||||||
['tbody', new Set(['tbody', 'tfoot'])],
|
|
||||||
['tfoot', new Set(['tbody'])],
|
|
||||||
['tr', new Set(['tr', 'tbody'])],
|
|
||||||
['td', new Set(['td', 'th', 'tr'])],
|
|
||||||
['th', new Set(['td', 'th', 'tr'])]
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// can this be a child of the parent element, or does it implicitly
|
// can this be a child of the parent element, or does it implicitly
|
||||||
// close it, like `<li>one<li>two`?
|
// close it, like `<li>one<li>two`?
|
||||||
export function closing_tag_omitted(current: string, next?: string) {
|
export function closing_tag_omitted(current: string, next?: string) {
|
||||||
if (disallowed_contents.has(current)) {
|
if (disallowed_contents.has(current)) {
|
||||||
if (!next || disallowed_contents.get(current).has(next)) {
|
if (!next || disallowed_contents.get(current).has(next)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import { TemplateNode } from '../../interfaces.js';
|
import { TemplateNode } from '../../interfaces.js';
|
||||||
|
|
||||||
export function to_string(node: TemplateNode) {
|
export function to_string(node: TemplateNode) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'IfBlock':
|
case 'IfBlock':
|
||||||
return '{#if} block';
|
return '{#if} block';
|
||||||
case 'ThenBlock':
|
case 'ThenBlock':
|
||||||
return '{:then} block';
|
return '{:then} block';
|
||||||
case 'ElseBlock':
|
case 'ElseBlock':
|
||||||
return '{:else} block';
|
return '{:else} block';
|
||||||
case 'PendingBlock':
|
case 'PendingBlock':
|
||||||
case 'AwaitBlock':
|
case 'AwaitBlock':
|
||||||
return '{#await} block';
|
return '{#await} block';
|
||||||
case 'CatchBlock':
|
case 'CatchBlock':
|
||||||
return '{:catch} block';
|
return '{:catch} block';
|
||||||
case 'EachBlock':
|
case 'EachBlock':
|
||||||
return '{#each} block';
|
return '{#each} block';
|
||||||
case 'RawMustacheTag':
|
case 'RawMustacheTag':
|
||||||
return '{@html} block';
|
return '{@html} block';
|
||||||
case 'DebugTag':
|
case 'DebugTag':
|
||||||
return '{@debug} block';
|
return '{@debug} block';
|
||||||
case 'Element':
|
case 'Element':
|
||||||
case 'InlineComponent':
|
case 'InlineComponent':
|
||||||
case 'Slot':
|
case 'Slot':
|
||||||
case 'Title':
|
case 'Title':
|
||||||
return `<${node.name}> tag`;
|
return `<${node.name}> tag`;
|
||||||
default:
|
default:
|
||||||
return node.type;
|
return node.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,39 +4,42 @@ import { locate } from 'locate-character';
|
||||||
import get_code_frame from './get_code_frame.js';
|
import get_code_frame from './get_code_frame.js';
|
||||||
|
|
||||||
export class CompileError extends Error {
|
export class CompileError extends Error {
|
||||||
code: string;
|
code: string;
|
||||||
start: { line: number; column: number };
|
start: { line: number; column: number };
|
||||||
end: { line: number; column: number };
|
end: { line: number; column: number };
|
||||||
pos: number;
|
pos: number;
|
||||||
filename: string;
|
filename: string;
|
||||||
frame: string;
|
frame: string;
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
|
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function error(message: string, props: {
|
export default function error(
|
||||||
name: string;
|
message: string,
|
||||||
code: string;
|
props: {
|
||||||
source: string;
|
name: string;
|
||||||
filename: string;
|
code: string;
|
||||||
start: number;
|
source: string;
|
||||||
end?: number;
|
filename: string;
|
||||||
}): never {
|
start: number;
|
||||||
const error = new CompileError(message);
|
end?: number;
|
||||||
error.name = props.name;
|
}
|
||||||
|
): never {
|
||||||
|
const error = new CompileError(message);
|
||||||
|
error.name = props.name;
|
||||||
|
|
||||||
const start = locate(props.source, props.start, { offsetLine: 1 });
|
const start = locate(props.source, props.start, { offsetLine: 1 });
|
||||||
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
|
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
|
||||||
|
|
||||||
error.code = props.code;
|
error.code = props.code;
|
||||||
error.start = start;
|
error.start = start;
|
||||||
error.end = end;
|
error.end = end;
|
||||||
error.pos = props.start;
|
error.pos = props.start;
|
||||||
error.filename = props.filename;
|
error.filename = props.filename;
|
||||||
|
|
||||||
error.frame = get_code_frame(props.source, start.line - 1, start.column);
|
error.frame = get_code_frame(props.source, start.line - 1, start.column);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
|
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
|
||||||
|
|
||||||
export default function full_char_code_at(str: string, i: number): number {
|
export default function full_char_code_at(str: string, i: number): number {
|
||||||
const code = str.charCodeAt(i);
|
const code = str.charCodeAt(i);
|
||||||
if (code <= 0xd7ff || code >= 0xe000) return code;
|
if (code <= 0xd7ff || code >= 0xe000) return code;
|
||||||
|
|
||||||
const next = str.charCodeAt(i + 1);
|
const next = str.charCodeAt(i + 1);
|
||||||
return (code << 10) + next - 0x35fdc00;
|
return (code << 10) + next - 0x35fdc00;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
export default function fuzzymatch(name: string, names: string[]) {
|
export default function fuzzymatch(name: string, names: string[]) {
|
||||||
const set = new FuzzySet(names);
|
const set = new FuzzySet(names);
|
||||||
const matches = set.get(name);
|
const matches = set.get(name);
|
||||||
|
|
||||||
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
|
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
|
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
|
||||||
|
@ -15,225 +15,215 @@ const GRAM_SIZE_UPPER = 3;
|
||||||
|
|
||||||
// return an edit distance from 0 to 1
|
// return an edit distance from 0 to 1
|
||||||
function _distance(str1: string, str2: string) {
|
function _distance(str1: string, str2: string) {
|
||||||
if (str1 === null && str2 === null) {
|
if (str1 === null && str2 === null) {
|
||||||
throw 'Trying to compare two null values';
|
throw 'Trying to compare two null values';
|
||||||
}
|
}
|
||||||
if (str1 === null || str2 === null) return 0;
|
if (str1 === null || str2 === null) return 0;
|
||||||
str1 = String(str1);
|
str1 = String(str1);
|
||||||
str2 = String(str2);
|
str2 = String(str2);
|
||||||
|
|
||||||
const distance = levenshtein(str1, str2);
|
const distance = levenshtein(str1, str2);
|
||||||
if (str1.length > str2.length) {
|
if (str1.length > str2.length) {
|
||||||
return 1 - distance / str1.length;
|
return 1 - distance / str1.length;
|
||||||
} else {
|
} else {
|
||||||
return 1 - distance / str2.length;
|
return 1 - distance / str2.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
function levenshtein(str1: string, str2: string) {
|
function levenshtein(str1: string, str2: string) {
|
||||||
const current: number[] = [];
|
const current: number[] = [];
|
||||||
let prev;
|
let prev;
|
||||||
let value;
|
let value;
|
||||||
|
|
||||||
for (let i = 0; i <= str2.length; i++) {
|
for (let i = 0; i <= str2.length; i++) {
|
||||||
for (let j = 0; j <= str1.length; j++) {
|
for (let j = 0; j <= str1.length; j++) {
|
||||||
if (i && j) {
|
if (i && j) {
|
||||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||||
value = prev;
|
value = prev;
|
||||||
} else {
|
} else {
|
||||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = i + j;
|
value = i + j;
|
||||||
}
|
}
|
||||||
|
|
||||||
prev = current[j];
|
prev = current[j];
|
||||||
current[j] = value;
|
current[j] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return current.pop();
|
return current.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const non_word_regex = /[^\w, ]+/;
|
const non_word_regex = /[^\w, ]+/;
|
||||||
|
|
||||||
function iterate_grams(value: string, gram_size = 2) {
|
function iterate_grams(value: string, gram_size = 2) {
|
||||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||||
const len_diff = gram_size - simplified.length;
|
const len_diff = gram_size - simplified.length;
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
if (len_diff > 0) {
|
if (len_diff > 0) {
|
||||||
for (let i = 0; i < len_diff; ++i) {
|
for (let i = 0; i < len_diff; ++i) {
|
||||||
value += '-';
|
value += '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||||
results.push(simplified.slice(i, i + gram_size));
|
results.push(simplified.slice(i, i + gram_size));
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gram_counter(value: string, gram_size = 2) {
|
function gram_counter(value: string, gram_size = 2) {
|
||||||
// return an object where key=gram, value=number of occurrences
|
// return an object where key=gram, value=number of occurrences
|
||||||
const result = {};
|
const result = {};
|
||||||
const grams = iterate_grams(value, gram_size);
|
const grams = iterate_grams(value, gram_size);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for (i; i < grams.length; ++i) {
|
for (i; i < grams.length; ++i) {
|
||||||
if (grams[i] in result) {
|
if (grams[i] in result) {
|
||||||
result[grams[i]] += 1;
|
result[grams[i]] += 1;
|
||||||
} else {
|
} else {
|
||||||
result[grams[i]] = 1;
|
result[grams[i]] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sort_descending(a, b) {
|
function sort_descending(a, b) {
|
||||||
return b[0] - a[0];
|
return b[0] - a[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
class FuzzySet {
|
class FuzzySet {
|
||||||
exact_set = {};
|
exact_set = {};
|
||||||
match_dict = {};
|
match_dict = {};
|
||||||
items = {};
|
items = {};
|
||||||
|
|
||||||
constructor(arr: string[]) {
|
constructor(arr: string[]) {
|
||||||
// initialization
|
// initialization
|
||||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||||
this.items[i] = [];
|
this.items[i] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// add all the items to the set
|
// add all the items to the set
|
||||||
for (let i = 0; i < arr.length; ++i) {
|
for (let i = 0; i < arr.length; ++i) {
|
||||||
this.add(arr[i]);
|
this.add(arr[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(value: string) {
|
add(value: string) {
|
||||||
const normalized_value = value.toLowerCase();
|
const normalized_value = value.toLowerCase();
|
||||||
if (normalized_value in this.exact_set) {
|
if (normalized_value in this.exact_set) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = GRAM_SIZE_LOWER;
|
let i = GRAM_SIZE_LOWER;
|
||||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||||
this._add(value, i);
|
this._add(value, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_add(value: string, gram_size: number) {
|
_add(value: string, gram_size: number) {
|
||||||
const normalized_value = value.toLowerCase();
|
const normalized_value = value.toLowerCase();
|
||||||
const items = this.items[gram_size] || [];
|
const items = this.items[gram_size] || [];
|
||||||
const index = items.length;
|
const index = items.length;
|
||||||
|
|
||||||
items.push(0);
|
items.push(0);
|
||||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||||
let sum_of_square_gram_counts = 0;
|
let sum_of_square_gram_counts = 0;
|
||||||
let gram;
|
let gram;
|
||||||
let gram_count;
|
let gram_count;
|
||||||
|
|
||||||
for (gram in gram_counts) {
|
for (gram in gram_counts) {
|
||||||
gram_count = gram_counts[gram];
|
gram_count = gram_counts[gram];
|
||||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||||
if (gram in this.match_dict) {
|
if (gram in this.match_dict) {
|
||||||
this.match_dict[gram].push([index, gram_count]);
|
this.match_dict[gram].push([index, gram_count]);
|
||||||
} else {
|
} else {
|
||||||
this.match_dict[gram] = [[index, gram_count]];
|
this.match_dict[gram] = [[index, gram_count]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||||
items[index] = [vector_normal, normalized_value];
|
items[index] = [vector_normal, normalized_value];
|
||||||
this.items[gram_size] = items;
|
this.items[gram_size] = items;
|
||||||
this.exact_set[normalized_value] = value;
|
this.exact_set[normalized_value] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(value: string) {
|
get(value: string) {
|
||||||
const normalized_value = value.toLowerCase();
|
const normalized_value = value.toLowerCase();
|
||||||
const result = this.exact_set[normalized_value];
|
const result = this.exact_set[normalized_value];
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return [[1, result]];
|
return [[1, result]];
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = [];
|
let results = [];
|
||||||
// start with high gram size and if there are no results, go to lower gram sizes
|
// start with high gram size and if there are no results, go to lower gram sizes
|
||||||
for (
|
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
|
||||||
let gram_size = GRAM_SIZE_UPPER;
|
results = this.__get(value, gram_size);
|
||||||
gram_size >= GRAM_SIZE_LOWER;
|
if (results) {
|
||||||
--gram_size
|
return results;
|
||||||
) {
|
}
|
||||||
results = this.__get(value, gram_size);
|
}
|
||||||
if (results) {
|
return null;
|
||||||
return results;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
__get(value: string, gram_size: number) {
|
__get(value: string, gram_size: number) {
|
||||||
const normalized_value = value.toLowerCase();
|
const normalized_value = value.toLowerCase();
|
||||||
const matches = {};
|
const matches = {};
|
||||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||||
const items = this.items[gram_size];
|
const items = this.items[gram_size];
|
||||||
let sum_of_square_gram_counts = 0;
|
let sum_of_square_gram_counts = 0;
|
||||||
let gram;
|
let gram;
|
||||||
let gram_count;
|
let gram_count;
|
||||||
let i;
|
let i;
|
||||||
let index;
|
let index;
|
||||||
let other_gram_count;
|
let other_gram_count;
|
||||||
|
|
||||||
for (gram in gram_counts) {
|
for (gram in gram_counts) {
|
||||||
gram_count = gram_counts[gram];
|
gram_count = gram_counts[gram];
|
||||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||||
if (gram in this.match_dict) {
|
if (gram in this.match_dict) {
|
||||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||||
index = this.match_dict[gram][i][0];
|
index = this.match_dict[gram][i][0];
|
||||||
other_gram_count = this.match_dict[gram][i][1];
|
other_gram_count = this.match_dict[gram][i][1];
|
||||||
if (index in matches) {
|
if (index in matches) {
|
||||||
matches[index] += gram_count * other_gram_count;
|
matches[index] += gram_count * other_gram_count;
|
||||||
} else {
|
} else {
|
||||||
matches[index] = gram_count * other_gram_count;
|
matches[index] = gram_count * other_gram_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||||
let results = [];
|
let results = [];
|
||||||
let match_score;
|
let match_score;
|
||||||
|
|
||||||
// build a results list of [score, str]
|
// build a results list of [score, str]
|
||||||
for (const match_index in matches) {
|
for (const match_index in matches) {
|
||||||
match_score = matches[match_index];
|
match_score = matches[match_index];
|
||||||
results.push([
|
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
|
||||||
match_score / (vector_normal * items[match_index][0]),
|
}
|
||||||
items[match_index][1]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort(sort_descending);
|
results.sort(sort_descending);
|
||||||
|
|
||||||
let new_results = [];
|
let new_results = [];
|
||||||
const end_index = Math.min(50, results.length);
|
const end_index = Math.min(50, results.length);
|
||||||
// truncate somewhat arbitrarily to 50
|
// truncate somewhat arbitrarily to 50
|
||||||
for (let i = 0; i < end_index; ++i) {
|
for (let i = 0; i < end_index; ++i) {
|
||||||
new_results.push([
|
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
|
||||||
_distance(results[i][1], normalized_value),
|
}
|
||||||
results[i][1]
|
results = new_results;
|
||||||
]);
|
results.sort(sort_descending);
|
||||||
}
|
|
||||||
results = new_results;
|
|
||||||
results.sort(sort_descending);
|
|
||||||
|
|
||||||
new_results = [];
|
new_results = [];
|
||||||
for (let i = 0; i < results.length; ++i) {
|
for (let i = 0; i < results.length; ++i) {
|
||||||
if (results[i][0] == results[0][0]) {
|
if (results[i][0] == results[0][0]) {
|
||||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new_results;
|
return new_results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,27 @@
|
||||||
function tabs_to_spaces(str: string) {
|
function tabs_to_spaces(str: string) {
|
||||||
return str.replace(/^\t+/, match => match.split('\t').join(' '));
|
return str.replace(/^\t+/, (match) => match.split('\t').join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function get_code_frame(
|
export default function get_code_frame(source: string, line: number, column: number) {
|
||||||
source: string,
|
const lines = source.split('\n');
|
||||||
line: number,
|
|
||||||
column: number
|
|
||||||
) {
|
|
||||||
const lines = source.split('\n');
|
|
||||||
|
|
||||||
const frame_start = Math.max(0, line - 2);
|
const frame_start = Math.max(0, line - 2);
|
||||||
const frame_end = Math.min(line + 3, lines.length);
|
const frame_end = Math.min(line + 3, lines.length);
|
||||||
|
|
||||||
const digits = String(frame_end + 1).length;
|
const digits = String(frame_end + 1).length;
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
.slice(frame_start, frame_end)
|
.slice(frame_start, frame_end)
|
||||||
.map((str, i) => {
|
.map((str, i) => {
|
||||||
const isErrorLine = frame_start + i === line;
|
const isErrorLine = frame_start + i === line;
|
||||||
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
|
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
|
||||||
|
|
||||||
if (isErrorLine) {
|
if (isErrorLine) {
|
||||||
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
|
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
|
||||||
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
|
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${line_num}: ${tabs_to_spaces(str)}`;
|
return `${line_num}: ${tabs_to_spaces(str)}`;
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
|
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
|
||||||
prev.next = next;
|
prev.next = next;
|
||||||
if (next) next.prev = prev;
|
if (next) next.prev = prev;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
export default function list(items: string[], conjunction = 'or') {
|
export default function list(items: string[], conjunction = 'or') {
|
||||||
if (items.length === 1) return items[0];
|
if (items.length === 1) return items[0];
|
||||||
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[
|
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
|
||||||
items.length - 1
|
|
||||||
]}`;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,138 +2,138 @@ import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
||||||
import full_char_code_at from './full_char_code_at.js';
|
import full_char_code_at from './full_char_code_at.js';
|
||||||
|
|
||||||
export const globals = new Set([
|
export const globals = new Set([
|
||||||
'alert',
|
'alert',
|
||||||
'Array',
|
'Array',
|
||||||
'Boolean',
|
'Boolean',
|
||||||
'clearInterval',
|
'clearInterval',
|
||||||
'clearTimeout',
|
'clearTimeout',
|
||||||
'confirm',
|
'confirm',
|
||||||
'console',
|
'console',
|
||||||
'Date',
|
'Date',
|
||||||
'decodeURI',
|
'decodeURI',
|
||||||
'decodeURIComponent',
|
'decodeURIComponent',
|
||||||
'document',
|
'document',
|
||||||
'Element',
|
'Element',
|
||||||
'encodeURI',
|
'encodeURI',
|
||||||
'encodeURIComponent',
|
'encodeURIComponent',
|
||||||
'Error',
|
'Error',
|
||||||
'EvalError',
|
'EvalError',
|
||||||
'Event',
|
'Event',
|
||||||
'EventSource',
|
'EventSource',
|
||||||
'fetch',
|
'fetch',
|
||||||
'global',
|
'global',
|
||||||
'globalThis',
|
'globalThis',
|
||||||
'history',
|
'history',
|
||||||
'Infinity',
|
'Infinity',
|
||||||
'InternalError',
|
'InternalError',
|
||||||
'Intl',
|
'Intl',
|
||||||
'isFinite',
|
'isFinite',
|
||||||
'isNaN',
|
'isNaN',
|
||||||
'JSON',
|
'JSON',
|
||||||
'localStorage',
|
'localStorage',
|
||||||
'location',
|
'location',
|
||||||
'Map',
|
'Map',
|
||||||
'Math',
|
'Math',
|
||||||
'NaN',
|
'NaN',
|
||||||
'navigator',
|
'navigator',
|
||||||
'Number',
|
'Number',
|
||||||
'Node',
|
'Node',
|
||||||
'Object',
|
'Object',
|
||||||
'parseFloat',
|
'parseFloat',
|
||||||
'parseInt',
|
'parseInt',
|
||||||
'process',
|
'process',
|
||||||
'Promise',
|
'Promise',
|
||||||
'prompt',
|
'prompt',
|
||||||
'RangeError',
|
'RangeError',
|
||||||
'ReferenceError',
|
'ReferenceError',
|
||||||
'RegExp',
|
'RegExp',
|
||||||
'sessionStorage',
|
'sessionStorage',
|
||||||
'Set',
|
'Set',
|
||||||
'setInterval',
|
'setInterval',
|
||||||
'setTimeout',
|
'setTimeout',
|
||||||
'String',
|
'String',
|
||||||
'SyntaxError',
|
'SyntaxError',
|
||||||
'TypeError',
|
'TypeError',
|
||||||
'undefined',
|
'undefined',
|
||||||
'URIError',
|
'URIError',
|
||||||
'URL',
|
'URL',
|
||||||
'window'
|
'window',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const reserved = new Set([
|
export const reserved = new Set([
|
||||||
'arguments',
|
'arguments',
|
||||||
'await',
|
'await',
|
||||||
'break',
|
'break',
|
||||||
'case',
|
'case',
|
||||||
'catch',
|
'catch',
|
||||||
'class',
|
'class',
|
||||||
'const',
|
'const',
|
||||||
'continue',
|
'continue',
|
||||||
'debugger',
|
'debugger',
|
||||||
'default',
|
'default',
|
||||||
'delete',
|
'delete',
|
||||||
'do',
|
'do',
|
||||||
'else',
|
'else',
|
||||||
'enum',
|
'enum',
|
||||||
'eval',
|
'eval',
|
||||||
'export',
|
'export',
|
||||||
'extends',
|
'extends',
|
||||||
'false',
|
'false',
|
||||||
'finally',
|
'finally',
|
||||||
'for',
|
'for',
|
||||||
'function',
|
'function',
|
||||||
'if',
|
'if',
|
||||||
'implements',
|
'implements',
|
||||||
'import',
|
'import',
|
||||||
'in',
|
'in',
|
||||||
'instanceof',
|
'instanceof',
|
||||||
'interface',
|
'interface',
|
||||||
'let',
|
'let',
|
||||||
'new',
|
'new',
|
||||||
'null',
|
'null',
|
||||||
'package',
|
'package',
|
||||||
'private',
|
'private',
|
||||||
'protected',
|
'protected',
|
||||||
'public',
|
'public',
|
||||||
'return',
|
'return',
|
||||||
'static',
|
'static',
|
||||||
'super',
|
'super',
|
||||||
'switch',
|
'switch',
|
||||||
'this',
|
'this',
|
||||||
'throw',
|
'throw',
|
||||||
'true',
|
'true',
|
||||||
'try',
|
'try',
|
||||||
'typeof',
|
'typeof',
|
||||||
'var',
|
'var',
|
||||||
'void',
|
'void',
|
||||||
'while',
|
'while',
|
||||||
'with',
|
'with',
|
||||||
'yield'
|
'yield',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
|
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
|
||||||
|
|
||||||
export function is_void(name: string) {
|
export function is_void(name: string) {
|
||||||
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
|
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function is_valid(str: string): boolean {
|
export function is_valid(str: string): boolean {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < str.length) {
|
while (i < str.length) {
|
||||||
const code = full_char_code_at(str, i);
|
const code = full_char_code_at(str, i);
|
||||||
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
|
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
|
||||||
|
|
||||||
i += code <= 0xffff ? 1 : 2;
|
i += code <= 0xffff ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitize(name: string) {
|
export function sanitize(name: string) {
|
||||||
return name
|
return name
|
||||||
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
||||||
.replace(/^_/, '')
|
.replace(/^_/, '')
|
||||||
.replace(/_$/, '')
|
.replace(/_$/, '')
|
||||||
.replace(/^[0-9]/, '_$&');
|
.replace(/^[0-9]/, '_$&');
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,21 +8,6 @@ export const xlink = 'http://www.w3.org/1999/xlink';
|
||||||
export const xml = 'http://www.w3.org/XML/1998/namespace';
|
export const xml = 'http://www.w3.org/XML/1998/namespace';
|
||||||
export const xmlns = 'http://www.w3.org/2000/xmlns';
|
export const xmlns = 'http://www.w3.org/2000/xmlns';
|
||||||
|
|
||||||
export const valid_namespaces = [
|
export const valid_namespaces = ['foreign', 'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', foreign, html, mathml, svg, xlink, xml, xmlns];
|
||||||
'foreign',
|
|
||||||
'html',
|
|
||||||
'mathml',
|
|
||||||
'svg',
|
|
||||||
'xlink',
|
|
||||||
'xml',
|
|
||||||
'xmlns',
|
|
||||||
foreign,
|
|
||||||
html,
|
|
||||||
mathml,
|
|
||||||
svg,
|
|
||||||
xlink,
|
|
||||||
xml,
|
|
||||||
xmlns
|
|
||||||
];
|
|
||||||
|
|
||||||
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };
|
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
export function nodes_match(a, b) {
|
export function nodes_match(a, b) {
|
||||||
if (!!a !== !!b) return false;
|
if (!!a !== !!b) return false;
|
||||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||||
|
|
||||||
if (a && typeof a === 'object') {
|
if (a && typeof a === 'object') {
|
||||||
if (Array.isArray(a)) {
|
if (Array.isArray(a)) {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
return a.every((child, i) => nodes_match(child, b[i]));
|
return a.every((child, i) => nodes_match(child, b[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const a_keys = Object.keys(a).sort();
|
const a_keys = Object.keys(a).sort();
|
||||||
const b_keys = Object.keys(b).sort();
|
const b_keys = Object.keys(b).sort();
|
||||||
|
|
||||||
if (a_keys.length !== b_keys.length) return false;
|
if (a_keys.length !== b_keys.length) return false;
|
||||||
|
|
||||||
let i = a_keys.length;
|
let i = a_keys.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
const key = a_keys[i];
|
const key = a_keys[i];
|
||||||
if (b_keys[i] !== key) return false;
|
if (b_keys[i] !== key) return false;
|
||||||
|
|
||||||
if (key === 'start' || key === 'end') continue;
|
if (key === 'start' || key === 'end') continue;
|
||||||
|
|
||||||
if (!nodes_match(a[key], b[key])) {
|
if (!nodes_match(a[key], b[key])) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return a === b;
|
return a === b;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { whitespace } from './patterns.js';
|
import { whitespace } from './patterns.js';
|
||||||
|
|
||||||
export function trim_start(str: string) {
|
export function trim_start(str: string) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (whitespace.test(str[i])) i += 1;
|
while (whitespace.test(str[i])) i += 1;
|
||||||
|
|
||||||
return str.slice(i);
|
return str.slice(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trim_end(str: string) {
|
export function trim_end(str: string) {
|
||||||
let i = str.length;
|
let i = str.length;
|
||||||
while (whitespace.test(str[i - 1])) i -= 1;
|
while (whitespace.test(str[i - 1])) i -= 1;
|
||||||
|
|
||||||
return str.slice(0, i);
|
return str.slice(0, i);
|
||||||
}
|
}
|
||||||
|
|
22
src/config.ts
Normal file
22
src/config.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import type { AstroConfig } from './@types/astro';
|
||||||
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
export async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
|
||||||
|
if (typeof rawRoot === 'undefined') {
|
||||||
|
rawRoot = process.cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = pathResolve(rawRoot);
|
||||||
|
const fileProtocolRoot = `file://${root}/`;
|
||||||
|
const astroConfigPath = pathJoin(root, 'astro.config.mjs');
|
||||||
|
|
||||||
|
if (!existsSync(astroConfigPath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const astroConfig: AstroConfig = (await import(astroConfigPath)).default;
|
||||||
|
astroConfig.projectRoot = new URL(astroConfig.projectRoot + '/', fileProtocolRoot);
|
||||||
|
astroConfig.hmxRoot = new URL(astroConfig.hmxRoot + '/', fileProtocolRoot);
|
||||||
|
return astroConfig;
|
||||||
|
}
|
107
src/dev.ts
107
src/dev.ts
|
@ -1,12 +1,11 @@
|
||||||
import type { AstroConfig } from './@types/astro';
|
import type { AstroConfig } from './@types/astro';
|
||||||
import type { LogOptions } from './logger.js';
|
import type { LogOptions } from './logger.js';
|
||||||
import { loadConfiguration, startServer as startSnowpackServer, logger as snowpackLogger } from 'snowpack';
|
|
||||||
import { existsSync, promises as fsPromises } from 'fs';
|
import { logger as snowpackLogger } from 'snowpack';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { relative as pathRelative } from 'path';
|
import { relative as pathRelative } from 'path';
|
||||||
import { defaultLogDestination, info, error, parseError } from './logger.js';
|
import { defaultLogDestination, error, parseError } from './logger.js';
|
||||||
|
import { createRuntime } from './runtime.js';
|
||||||
const { readFile } = fsPromises;
|
|
||||||
|
|
||||||
const hostname = '127.0.0.1';
|
const hostname = '127.0.0.1';
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
@ -20,95 +19,45 @@ const logging: LogOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function (astroConfig: AstroConfig) {
|
export default async function (astroConfig: AstroConfig) {
|
||||||
const { projectRoot, hmxRoot } = astroConfig;
|
const { projectRoot } = astroConfig;
|
||||||
|
|
||||||
const internalPath = new URL('./frontend/', import.meta.url);
|
const runtime = await createRuntime(astroConfig, logging);
|
||||||
const snowpackConfigPath = new URL('./snowpack.config.js', projectRoot);
|
|
||||||
|
|
||||||
// Workaround for SKY-251
|
|
||||||
const hmxPlugOptions: { resolve?: (s: string) => string } = {};
|
|
||||||
if (existsSync(new URL('./package-lock.json', projectRoot))) {
|
|
||||||
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
|
|
||||||
const pkgLock = JSON.parse(pkgLockStr);
|
|
||||||
hmxPlugOptions.resolve = (pkgName: string) => {
|
|
||||||
const ver = pkgLock.dependencies[pkgName].version;
|
|
||||||
return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const snowpackConfig = await loadConfiguration(
|
|
||||||
{
|
|
||||||
root: projectRoot.pathname,
|
|
||||||
mount: {
|
|
||||||
[hmxRoot.pathname]: '/_hmx',
|
|
||||||
[internalPath.pathname]: '/__hmx_internal__',
|
|
||||||
},
|
|
||||||
plugins: [['astro/snowpack-plugin', hmxPlugOptions]],
|
|
||||||
devOptions: {
|
|
||||||
open: 'none',
|
|
||||||
output: 'stream',
|
|
||||||
port: 0,
|
|
||||||
},
|
|
||||||
packageOptions: {
|
|
||||||
knownEntrypoints: ['preact-render-to-string'],
|
|
||||||
external: ['@vue/server-renderer'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
snowpackConfigPath.pathname
|
|
||||||
);
|
|
||||||
const snowpack = await startSnowpackServer({
|
|
||||||
config: snowpackConfig,
|
|
||||||
lockfile: null,
|
|
||||||
});
|
|
||||||
const runtime = snowpack.getServerRuntime();
|
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
const fullurl = new URL(req.url || '/', 'https://example.org/');
|
const result = await runtime.load(req.url);
|
||||||
const reqPath = decodeURI(fullurl.pathname);
|
|
||||||
const selectedPage = reqPath.substr(1) || 'index';
|
|
||||||
info(logging, 'access', reqPath);
|
|
||||||
|
|
||||||
const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot);
|
switch (result.statusCode) {
|
||||||
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot);
|
case 200: {
|
||||||
const selectedPageUrl = `/_hmx/pages/${selectedPage}.js`;
|
|
||||||
|
|
||||||
// Non-hmx pages
|
|
||||||
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
|
|
||||||
try {
|
|
||||||
const result = await snowpack.loadUrl(reqPath);
|
|
||||||
if (result.contentType) {
|
if (result.contentType) {
|
||||||
res.setHeader('Content-Type', result.contentType);
|
res.setHeader('Content-Type', result.contentType);
|
||||||
}
|
}
|
||||||
res.write(result.contents);
|
res.write(result.contents);
|
||||||
res.end();
|
res.end();
|
||||||
} catch (err) {
|
break;
|
||||||
|
}
|
||||||
|
case 404: {
|
||||||
|
const fullurl = new URL(req.url || '/', 'https://example.org/');
|
||||||
|
const reqPath = decodeURI(fullurl.pathname);
|
||||||
error(logging, 'static', 'Not found', reqPath);
|
error(logging, 'static', 'Not found', reqPath);
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.end('Not Found');
|
res.end('Not Found');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return;
|
case 500: {
|
||||||
}
|
switch (result.type) {
|
||||||
|
case 'parse-error': {
|
||||||
try {
|
const err = result.error;
|
||||||
const mod = await runtime.importModule(selectedPageUrl);
|
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
||||||
const html = await mod.exports.default();
|
parseError(logging, err);
|
||||||
res.statusCode = 200;
|
break;
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
}
|
||||||
res.end(html);
|
default: {
|
||||||
} catch (err) {
|
error(logging, 'executing hmx', result.error);
|
||||||
switch (err.code) {
|
break;
|
||||||
case 'parse-error': {
|
}
|
||||||
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
|
||||||
debugger;
|
|
||||||
parseError(logging, err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.error(err.code, err);
|
|
||||||
error(logging, 'running hmx', err);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,31 +1,32 @@
|
||||||
import type { CompileError } from './compiler/utils/error.js';
|
import type { CompileError } from './compiler/utils/error.js';
|
||||||
import { bold, blue, red, grey, underline } from 'kleur/colors';
|
import { bold, blue, red, grey, underline } from 'kleur/colors';
|
||||||
import { Writable } from 'stream';
|
import { Writable } from 'stream';
|
||||||
|
import { format as utilFormat } from 'util';
|
||||||
|
|
||||||
type ConsoleStream = Writable & {
|
type ConsoleStream = Writable & {
|
||||||
fd: 1 | 2
|
fd: 1 | 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultLogDestination = new Writable({
|
export const defaultLogDestination = new Writable({
|
||||||
objectMode: true,
|
objectMode: true,
|
||||||
write(event: LogMessage, _, callback) {
|
write(event: LogMessage, _, callback) {
|
||||||
let dest: ConsoleStream = process.stderr;
|
let dest: ConsoleStream = process.stderr;
|
||||||
if(levels[event.level] < levels['error']) {
|
if (levels[event.level] < levels['error']) {
|
||||||
dest = process.stdout;
|
dest = process.stdout;
|
||||||
}
|
}
|
||||||
let type = event.type;
|
let type = event.type;
|
||||||
if(event.level === 'info') {
|
if (event.level === 'info') {
|
||||||
type = bold(blue(type));
|
type = bold(blue(type));
|
||||||
} else if(event.level === 'error') {
|
} else if (event.level === 'error') {
|
||||||
type = bold(red(type));
|
type = bold(red(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
dest.write(`[${type}] `);
|
dest.write(`[${type}] `);
|
||||||
dest.write(event.message);
|
dest.write(utilFormat(...event.args));
|
||||||
dest.write('\n');
|
dest.write('\n');
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface LogWritable<T> extends Writable {
|
interface LogWritable<T> extends Writable {
|
||||||
|
@ -37,18 +38,19 @@ export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
export interface LogOptions {
|
export interface LogOptions {
|
||||||
dest: LogWritable<LogMessage>;
|
dest: LogWritable<LogMessage>;
|
||||||
level: LoggerLevel
|
level: LoggerLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLogOptions: LogOptions = {
|
export const defaultLogOptions: LogOptions = {
|
||||||
dest: defaultLogDestination,
|
dest: defaultLogDestination,
|
||||||
level: 'info'
|
level: 'info',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface LogMessage {
|
export interface LogMessage {
|
||||||
type: string;
|
type: string;
|
||||||
level: LoggerLevel,
|
level: LoggerLevel;
|
||||||
message: string;
|
message: string;
|
||||||
|
args: Array<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const levels: Record<LoggerLevel, number> = {
|
const levels: Record<LoggerLevel, number> = {
|
||||||
|
@ -59,19 +61,14 @@ const levels: Record<LoggerLevel, number> = {
|
||||||
silent: 90,
|
silent: 90,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...messages: Array<any>) {
|
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
|
||||||
let event: LogMessage = {
|
const event: LogMessage = {
|
||||||
type,
|
type,
|
||||||
level,
|
level,
|
||||||
message: ''
|
args,
|
||||||
|
message: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if(messages.length === 1 && typeof messages[0] === 'object') {
|
|
||||||
Object.assign(event, messages[0]);
|
|
||||||
} else {
|
|
||||||
event.message = messages.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// test if this level is enabled or not
|
// test if this level is enabled or not
|
||||||
if (levels[opts.level] > levels[level]) {
|
if (levels[opts.level] > levels[level]) {
|
||||||
return; // do nothing
|
return; // do nothing
|
||||||
|
@ -99,20 +96,24 @@ export function error(opts: LogOptions, type: string, ...messages: Array<any>) {
|
||||||
export function parseError(opts: LogOptions, err: CompileError) {
|
export function parseError(opts: LogOptions, err: CompileError) {
|
||||||
let frame = err.frame
|
let frame = err.frame
|
||||||
// Switch colons for pipes
|
// Switch colons for pipes
|
||||||
.replace(/^([0-9]+)(:)/mg, `${bold('$1')} │`)
|
.replace(/^([0-9]+)(:)/gm, `${bold('$1')} │`)
|
||||||
// Make the caret red.
|
// Make the caret red.
|
||||||
.replace(/(?<=^\s+)(\^)/mg, bold(red(' ^')))
|
.replace(/(?<=^\s+)(\^)/gm, bold(red(' ^')))
|
||||||
// Add identation
|
// Add identation
|
||||||
.replace(/^/mg, ' ');
|
.replace(/^/gm, ' ');
|
||||||
|
|
||||||
error(opts, 'parse-error', `
|
error(
|
||||||
|
opts,
|
||||||
|
'parse-error',
|
||||||
|
`
|
||||||
|
|
||||||
${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}
|
${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}
|
||||||
|
|
||||||
${bold(red(`𝘅 ${err.message}`))}
|
${bold(red(`𝘅 ${err.message}`))}
|
||||||
|
|
||||||
${frame}
|
${frame}
|
||||||
`);
|
`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A default logger for when too lazy to pass LogOptions around.
|
// A default logger for when too lazy to pass LogOptions around.
|
||||||
|
@ -120,5 +121,5 @@ export const logger = {
|
||||||
debug: debug.bind(null, defaultLogOptions),
|
debug: debug.bind(null, defaultLogOptions),
|
||||||
info: info.bind(null, defaultLogOptions),
|
info: info.bind(null, defaultLogOptions),
|
||||||
warn: warn.bind(null, defaultLogOptions),
|
warn: warn.bind(null, defaultLogOptions),
|
||||||
error: error.bind(null, defaultLogOptions)
|
error: error.bind(null, defaultLogOptions),
|
||||||
};
|
};
|
137
src/runtime.ts
Normal file
137
src/runtime.ts
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadResult as SnowpackLoadResult } from 'snowpack';
|
||||||
|
import type { AstroConfig } from './@types/astro';
|
||||||
|
import type { LogOptions } from './logger';
|
||||||
|
import type { CompileError } from './compiler/utils/error.js';
|
||||||
|
import { info, error, parseError } from './logger.js';
|
||||||
|
|
||||||
|
import { existsSync, promises as fsPromises } from 'fs';
|
||||||
|
import { loadConfiguration, startServer as startSnowpackServer } from 'snowpack';
|
||||||
|
|
||||||
|
const { readFile } = fsPromises;
|
||||||
|
|
||||||
|
interface RuntimeConfig {
|
||||||
|
astroConfig: AstroConfig;
|
||||||
|
logging: LogOptions;
|
||||||
|
snowpack: SnowpackDevServer;
|
||||||
|
snowpackRuntime: SnowpackServerRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadResultSuccess = {
|
||||||
|
statusCode: 200;
|
||||||
|
contents: string | Buffer;
|
||||||
|
contentType?: string | false;
|
||||||
|
};
|
||||||
|
type LoadResultNotFound = { statusCode: 404; error: Error };
|
||||||
|
type LoadResultError = { statusCode: 500 } & ({ type: 'parse-error'; error: CompileError } | { type: 'unknown'; error: Error });
|
||||||
|
|
||||||
|
export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultError;
|
||||||
|
|
||||||
|
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
||||||
|
const { logging, snowpack, snowpackRuntime } = config;
|
||||||
|
const { hmxRoot } = config.astroConfig;
|
||||||
|
|
||||||
|
const fullurl = new URL(rawPathname || '/', 'https://example.org/');
|
||||||
|
const reqPath = decodeURI(fullurl.pathname);
|
||||||
|
const selectedPage = reqPath.substr(1) || 'index';
|
||||||
|
info(logging, 'access', reqPath);
|
||||||
|
|
||||||
|
const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot);
|
||||||
|
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot);
|
||||||
|
const selectedPageUrl = `/_hmx/pages/${selectedPage}.js`;
|
||||||
|
|
||||||
|
// Non-hmx pages
|
||||||
|
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
|
||||||
|
try {
|
||||||
|
const result = await snowpack.loadUrl(reqPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
statusCode: 404,
|
||||||
|
error: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mod = await snowpackRuntime.importModule(selectedPageUrl);
|
||||||
|
const html = (await mod.exports.default()) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
contents: html,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
switch (err.code) {
|
||||||
|
case 'parse-error': {
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
type: 'parse-error',
|
||||||
|
error: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
type: 'unknown',
|
||||||
|
error: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRuntime(astroConfig: AstroConfig, logging: LogOptions) {
|
||||||
|
const { projectRoot, hmxRoot } = astroConfig;
|
||||||
|
|
||||||
|
const internalPath = new URL('./frontend/', import.meta.url);
|
||||||
|
|
||||||
|
// Workaround for SKY-251
|
||||||
|
const hmxPlugOptions: { resolve?: (s: string) => string } = {};
|
||||||
|
if (existsSync(new URL('./package-lock.json', projectRoot))) {
|
||||||
|
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
|
||||||
|
const pkgLock = JSON.parse(pkgLockStr);
|
||||||
|
hmxPlugOptions.resolve = (pkgName: string) => {
|
||||||
|
const ver = pkgLock.dependencies[pkgName].version;
|
||||||
|
return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const snowpackConfig = await loadConfiguration({
|
||||||
|
root: projectRoot.pathname,
|
||||||
|
mount: {
|
||||||
|
[hmxRoot.pathname]: '/_hmx',
|
||||||
|
[internalPath.pathname]: '/__hmx_internal__',
|
||||||
|
},
|
||||||
|
plugins: [[new URL('../snowpack-plugin.cjs', import.meta.url).pathname, hmxPlugOptions]],
|
||||||
|
devOptions: {
|
||||||
|
open: 'none',
|
||||||
|
output: 'stream',
|
||||||
|
port: 0,
|
||||||
|
},
|
||||||
|
packageOptions: {
|
||||||
|
knownEntrypoints: ['preact-render-to-string'],
|
||||||
|
external: ['@vue/server-renderer'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const snowpack = await startSnowpackServer({
|
||||||
|
config: snowpackConfig,
|
||||||
|
lockfile: null,
|
||||||
|
});
|
||||||
|
const snowpackRuntime = snowpack.getServerRuntime();
|
||||||
|
|
||||||
|
const runtimeConfig: RuntimeConfig = {
|
||||||
|
astroConfig,
|
||||||
|
logging,
|
||||||
|
snowpack,
|
||||||
|
snowpackRuntime,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: load.bind(null, runtimeConfig),
|
||||||
|
shutdown: () => snowpack.shutdown(),
|
||||||
|
};
|
||||||
|
}
|
0
src/style-stuff.ts
Normal file
0
src/style-stuff.ts
Normal file
6
test/fixtures/hmx-basic/astro.config.mjs
vendored
Normal file
6
test/fixtures/hmx-basic/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
export default {
|
||||||
|
projectRoot: '.',
|
||||||
|
hmxRoot: './astro',
|
||||||
|
dist: './_site'
|
||||||
|
}
|
15
test/fixtures/hmx-basic/astro/pages/index.hmx
vendored
Normal file
15
test/fixtures/hmx-basic/astro/pages/index.hmx
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script hmx="setup">
|
||||||
|
export function setup() {
|
||||||
|
return {
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- Head Stuff -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
</body>
|
5
test/fixtures/hmx-basic/snowpack.config.js
vendored
Normal file
5
test/fixtures/hmx-basic/snowpack.config.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
mount: {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
38
test/hmx-basic.test.js
Normal file
38
test/hmx-basic.test.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { suite } from 'uvu';
|
||||||
|
import * as assert from 'uvu/assert';
|
||||||
|
import { createRuntime } from '../lib/runtime.js';
|
||||||
|
import { doc } from './test-utils.js';
|
||||||
|
|
||||||
|
const Basics = suite('HMX Basics');
|
||||||
|
|
||||||
|
let runtime;
|
||||||
|
|
||||||
|
Basics.before(async () => {
|
||||||
|
const astroConfig = {
|
||||||
|
projectRoot: new URL('./fixtures/hmx-basic/', import.meta.url),
|
||||||
|
hmxRoot: new URL('./fixtures/hmx-basic/astro/', import.meta.url),
|
||||||
|
dist: './_site'
|
||||||
|
};
|
||||||
|
|
||||||
|
const logging = {
|
||||||
|
level: 'error',
|
||||||
|
dest: process.stderr
|
||||||
|
};
|
||||||
|
|
||||||
|
runtime = await createRuntime(astroConfig, logging);
|
||||||
|
});
|
||||||
|
|
||||||
|
Basics.after(async () => {
|
||||||
|
await runtime.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
Basics('Can load hmx page', async () => {
|
||||||
|
const result = await runtime.load('/');
|
||||||
|
|
||||||
|
assert.equal(result.statusCode, 200);
|
||||||
|
const $ = doc(result.contents);
|
||||||
|
|
||||||
|
assert.equal($('h1').text(), 'Hello world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
Basics.run();
|
5
test/test-utils.js
Normal file
5
test/test-utils.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
|
||||||
|
export function doc(html) {
|
||||||
|
return cheerio.load(html);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue