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": {
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
|
@ -684,67 +685,31 @@
|
|||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
|
||||
},
|
||||
"cheerio": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
|
||||
"version": "1.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-select": "~1.2.0",
|
||||
"dom-serializer": "~0.1.0",
|
||||
"entities": "~1.1.1",
|
||||
"htmlparser2": "^3.9.1",
|
||||
"lodash.assignin": "^4.0.9",
|
||||
"lodash.bind": "^4.1.4",
|
||||
"lodash.defaults": "^4.0.1",
|
||||
"lodash.filter": "^4.4.0",
|
||||
"lodash.flatten": "^4.2.0",
|
||||
"lodash.foreach": "^4.3.0",
|
||||
"lodash.map": "^4.4.0",
|
||||
"lodash.merge": "^4.4.0",
|
||||
"lodash.pick": "^4.2.1",
|
||||
"lodash.reduce": "^4.4.0",
|
||||
"lodash.reject": "^4.4.0",
|
||||
"lodash.some": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"domhandler": {
|
||||
"version": "2.4.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"cheerio-select-tmp": "^0.1.0",
|
||||
"dom-serializer": "~1.2.0",
|
||||
"domhandler": "^4.0.0",
|
||||
"entities": "~2.1.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"parse5": "^6.0.0",
|
||||
"parse5-htmlparser2-tree-adapter": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"cheerio-select-tmp": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz",
|
||||
"integrity": "sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-select": "^3.1.2",
|
||||
"css-what": "^4.0.0",
|
||||
"domelementtype": "^2.1.0",
|
||||
"domhandler": "^4.0.0",
|
||||
"domutils": "^2.4.4"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
|
@ -950,14 +915,16 @@
|
|||
"dev": true
|
||||
},
|
||||
"css-select": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz",
|
||||
"integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boolbase": "~1.0.0",
|
||||
"css-what": "2.1",
|
||||
"domutils": "1.5.1",
|
||||
"nth-check": "~1.0.1"
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^4.0.0",
|
||||
"domhandler": "^4.0.0",
|
||||
"domutils": "^2.4.3",
|
||||
"nth-check": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"css-tree": {
|
||||
|
@ -970,9 +937,10 @@
|
|||
}
|
||||
},
|
||||
"css-what": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
|
||||
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz",
|
||||
"integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==",
|
||||
"dev": true
|
||||
},
|
||||
"cssesc": {
|
||||
"version": "3.0.0",
|
||||
|
@ -1019,6 +987,11 @@
|
|||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz",
|
||||
|
@ -1045,6 +1018,18 @@
|
|||
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
@ -1064,18 +1049,21 @@
|
|||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
|
||||
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
|
||||
"integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^1.3.0",
|
||||
"entities": "^1.1.1"
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
|
||||
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
|
||||
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
|
||||
"dev": true
|
||||
},
|
||||
"domhandler": {
|
||||
"version": "4.0.0",
|
||||
|
@ -1093,12 +1081,14 @@
|
|||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
|
||||
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.0.tgz",
|
||||
"integrity": "sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-serializer": "0",
|
||||
"domelementtype": "1"
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"dot-prop": {
|
||||
|
@ -1151,9 +1141,10 @@
|
|||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||
"dev": true
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
|
@ -1806,7 +1797,8 @@
|
|||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"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": {
|
||||
"version": "1.3.7",
|
||||
|
@ -2073,71 +2065,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"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": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
@ -2292,6 +2224,12 @@
|
|||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
@ -2390,11 +2328,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"nth-check": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
|
||||
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
|
||||
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boolbase": "~1.0.0"
|
||||
"boolbase": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"object-assign": {
|
||||
|
@ -2499,6 +2438,21 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
|
@ -2873,10 +2827,14 @@
|
|||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
"sade": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
|
||||
"integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mri": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.32.8",
|
||||
|
@ -3216,6 +3174,12 @@
|
|||
"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": {
|
||||
"version": "3.1.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"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'",
|
||||
"format": "prettier -w 'src/**/*.{js,ts}'",
|
||||
"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": {
|
||||
"@types/estree": "0.0.46",
|
||||
|
@ -31,8 +32,8 @@
|
|||
"acorn-jsx": "^5.3.1",
|
||||
"astring": "^1.7.0",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"cheerio": "^0.22.0",
|
||||
"css-tree": "^1.1.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"domhandler": "^4.0.0",
|
||||
"es-module-lexer": "^0.4.1",
|
||||
"gray-matter": "^4.0.2",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"@types/yargs-parser": "^20.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||
"@typescript-eslint/parser": "^4.18.0",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"concurrently": "^6.0.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^7.22.0",
|
||||
|
@ -67,6 +69,7 @@
|
|||
"preact": "^10.5.12",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"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 * as colors from 'kleur/colors';
|
||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||
import { existsSync, promises as fsPromises } from 'fs';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import yargs from 'yargs-parser';
|
||||
|
||||
import { loadConfig } from './config.js';
|
||||
import generate from './generate.js';
|
||||
import devServer from './dev.js';
|
||||
|
||||
|
@ -49,25 +49,6 @@ async function printVersion() {
|
|||
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>) {
|
||||
const astroConfig = await loadConfig(rawRoot);
|
||||
if (typeof astroConfig === 'undefined') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { CompileOptions } from '../@types/compiler';
|
||||
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 esbuild from 'esbuild';
|
||||
|
@ -61,7 +61,6 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
|||
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
|
||||
continue;
|
||||
default:
|
||||
console.log(val);
|
||||
throw new Error('UNKNOWN V');
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +74,6 @@ function getTextFromAttribute(attr: any): string {
|
|||
if (attr.data !== undefined) {
|
||||
return attr.data;
|
||||
}
|
||||
console.log(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> {
|
||||
await eslexer.init;
|
||||
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
|
||||
|
||||
// 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
|
||||
|
||||
const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
|
||||
const components = Object.fromEntries(
|
||||
scriptImports.map((imp) => {
|
||||
|
@ -193,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
|
||||
walk(ast.html, {
|
||||
enter(node: TemplateNode) {
|
||||
// console.log("enter", node.type);
|
||||
switch (node.type) {
|
||||
case 'MustacheTag':
|
||||
let code = compileScriptSafe(node.expression, 'jsx');
|
||||
|
@ -238,7 +234,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
case 'Element':
|
||||
const name: string = node.name;
|
||||
if (!name) {
|
||||
console.log(node);
|
||||
throw new Error('AHHHH');
|
||||
}
|
||||
const attributes = getAttributes(node.attributes);
|
||||
|
@ -298,12 +293,10 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
|
|||
return;
|
||||
}
|
||||
default:
|
||||
console.log(node);
|
||||
throw new Error('Unexpected node type: ' + node.type);
|
||||
}
|
||||
},
|
||||
leave(node, parent, prop, index) {
|
||||
// console.log("leave", node.type);
|
||||
switch (node.type) {
|
||||
case 'Text':
|
||||
case 'MustacheTag':
|
||||
|
|
|
@ -1,75 +1,82 @@
|
|||
// @ts-nocheck
|
||||
|
||||
const now = (typeof process !== 'undefined' && process.hrtime)
|
||||
? () => {
|
||||
const t = process.hrtime();
|
||||
return t[0] * 1e3 + t[1] / 1e6;
|
||||
}
|
||||
: () => self.performance.now();
|
||||
const now =
|
||||
typeof process !== 'undefined' && process.hrtime
|
||||
? () => {
|
||||
const t = process.hrtime();
|
||||
return t[0] * 1e3 + t[1] / 1e6;
|
||||
}
|
||||
: () => self.performance.now();
|
||||
|
||||
interface Timing {
|
||||
label: string;
|
||||
start: number;
|
||||
end: number;
|
||||
children: Timing[];
|
||||
label: string;
|
||||
start: number;
|
||||
end: number;
|
||||
children: Timing[];
|
||||
}
|
||||
|
||||
function collapse_timings(timings) {
|
||||
const result = {};
|
||||
timings.forEach(timing => {
|
||||
result[timing.label] = Object.assign({
|
||||
total: timing.end - timing.start
|
||||
}, timing.children && collapse_timings(timing.children));
|
||||
});
|
||||
return result;
|
||||
const result = {};
|
||||
timings.forEach((timing) => {
|
||||
result[timing.label] = Object.assign(
|
||||
{
|
||||
total: timing.end - timing.start,
|
||||
},
|
||||
timing.children && collapse_timings(timing.children)
|
||||
);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Stats {
|
||||
start_time: number;
|
||||
current_timing: Timing;
|
||||
current_children: Timing[];
|
||||
timings: Timing[];
|
||||
stack: Timing[];
|
||||
start_time: number;
|
||||
current_timing: Timing;
|
||||
current_children: Timing[];
|
||||
timings: Timing[];
|
||||
stack: Timing[];
|
||||
|
||||
constructor() {
|
||||
this.start_time = now();
|
||||
this.stack = [];
|
||||
this.current_children = this.timings = [];
|
||||
}
|
||||
constructor() {
|
||||
this.start_time = now();
|
||||
this.stack = [];
|
||||
this.current_children = this.timings = [];
|
||||
}
|
||||
|
||||
start(label) {
|
||||
const timing = {
|
||||
label,
|
||||
start: now(),
|
||||
end: null,
|
||||
children: []
|
||||
};
|
||||
start(label) {
|
||||
const timing = {
|
||||
label,
|
||||
start: now(),
|
||||
end: null,
|
||||
children: [],
|
||||
};
|
||||
|
||||
this.current_children.push(timing);
|
||||
this.stack.push(timing);
|
||||
this.current_children.push(timing);
|
||||
this.stack.push(timing);
|
||||
|
||||
this.current_timing = timing;
|
||||
this.current_children = timing.children;
|
||||
}
|
||||
this.current_timing = timing;
|
||||
this.current_children = timing.children;
|
||||
}
|
||||
|
||||
stop(label) {
|
||||
if (label !== this.current_timing.label) {
|
||||
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
|
||||
}
|
||||
stop(label) {
|
||||
if (label !== this.current_timing.label) {
|
||||
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
|
||||
}
|
||||
|
||||
this.current_timing.end = now();
|
||||
this.stack.pop();
|
||||
this.current_timing = this.stack[this.stack.length - 1];
|
||||
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
|
||||
}
|
||||
this.current_timing.end = now();
|
||||
this.stack.pop();
|
||||
this.current_timing = this.stack[this.stack.length - 1];
|
||||
this.current_children = this.current_timing ? this.current_timing.children : this.timings;
|
||||
}
|
||||
|
||||
render() {
|
||||
const timings = Object.assign({
|
||||
total: now() - this.start_time
|
||||
}, collapse_timings(this.timings));
|
||||
render() {
|
||||
const timings = Object.assign(
|
||||
{
|
||||
total: now() - this.start_time,
|
||||
},
|
||||
collapse_timings(this.timings)
|
||||
);
|
||||
|
||||
return {
|
||||
timings
|
||||
};
|
||||
}
|
||||
return {
|
||||
timings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,16 @@ import jsx from 'acorn-jsx';
|
|||
|
||||
const acornJsx = acorn.Parser.extend(jsx());
|
||||
|
||||
export const parse = (source: string): Node => acorn.parse(source, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
locations: true
|
||||
});
|
||||
export const parse = (source: string): Node =>
|
||||
acorn.parse(source, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
locations: true,
|
||||
});
|
||||
|
||||
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
locations: true
|
||||
});
|
||||
export const parse_expression_at = (source: string, index: number): Node =>
|
||||
acornJsx.parseExpressionAt(source, index, {
|
||||
sourceType: 'module',
|
||||
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 error from '../utils/error.js';
|
||||
|
||||
type ParserState = (parser: Parser) => (ParserState | void);
|
||||
type ParserState = (parser: Parser) => ParserState | void;
|
||||
|
||||
interface LastAutoClosedTag {
|
||||
tag: string;
|
||||
reason: string;
|
||||
depth: number;
|
||||
tag: string;
|
||||
reason: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
readonly template: string;
|
||||
readonly filename?: string;
|
||||
readonly customElement: boolean;
|
||||
readonly template: string;
|
||||
readonly filename?: string;
|
||||
readonly customElement: boolean;
|
||||
|
||||
index = 0;
|
||||
stack: TemplateNode[] = [];
|
||||
index = 0;
|
||||
stack: TemplateNode[] = [];
|
||||
|
||||
html: Fragment;
|
||||
css: Style[] = [];
|
||||
js: Script[] = [];
|
||||
meta_tags = {};
|
||||
last_auto_closed_tag?: LastAutoClosedTag;
|
||||
html: Fragment;
|
||||
css: Style[] = [];
|
||||
js: Script[] = [];
|
||||
meta_tags = {};
|
||||
last_auto_closed_tag?: LastAutoClosedTag;
|
||||
|
||||
constructor(template: string, options: ParserOptions) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Template must be a string');
|
||||
}
|
||||
constructor(template: string, options: ParserOptions) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Template must be a string');
|
||||
}
|
||||
|
||||
this.template = template.replace(/\s+$/, '');
|
||||
this.filename = options.filename;
|
||||
this.customElement = options.customElement;
|
||||
this.template = template.replace(/\s+$/, '');
|
||||
this.filename = options.filename;
|
||||
this.customElement = options.customElement;
|
||||
|
||||
this.html = {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'Fragment',
|
||||
children: []
|
||||
};
|
||||
this.html = {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'Fragment',
|
||||
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) {
|
||||
state = state(this) || fragment;
|
||||
}
|
||||
while (this.index < this.template.length) {
|
||||
state = state(this) || fragment;
|
||||
}
|
||||
|
||||
if (this.stack.length > 1) {
|
||||
const current = this.current();
|
||||
if (this.stack.length > 1) {
|
||||
const current = this.current();
|
||||
|
||||
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
|
||||
const slug = current.type === 'Element' ? 'element' : 'block';
|
||||
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
|
||||
const slug = current.type === 'Element' ? 'element' : 'block';
|
||||
|
||||
this.error({
|
||||
code: `unclosed-${slug}`,
|
||||
message: `${type} was left open`
|
||||
}, current.start);
|
||||
}
|
||||
this.error(
|
||||
{
|
||||
code: `unclosed-${slug}`,
|
||||
message: `${type} was left open`,
|
||||
},
|
||||
current.start
|
||||
);
|
||||
}
|
||||
|
||||
if (state !== fragment) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
}
|
||||
if (state !== fragment) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.html.children.length) {
|
||||
let start = this.html.children[0].start;
|
||||
while (whitespace.test(template[start])) start += 1;
|
||||
if (this.html.children.length) {
|
||||
let start = this.html.children[0].start;
|
||||
while (whitespace.test(template[start])) start += 1;
|
||||
|
||||
let end = this.html.children[this.html.children.length - 1].end;
|
||||
while (whitespace.test(template[end - 1])) end -= 1;
|
||||
let end = this.html.children[this.html.children.length - 1].end;
|
||||
while (whitespace.test(template[end - 1])) end -= 1;
|
||||
|
||||
this.html.start = start;
|
||||
this.html.end = end;
|
||||
} else {
|
||||
this.html.start = this.html.end = null;
|
||||
}
|
||||
}
|
||||
this.html.start = start;
|
||||
this.html.end = end;
|
||||
} else {
|
||||
this.html.start = this.html.end = null;
|
||||
}
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
current() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
acorn_error(err: any) {
|
||||
this.error({
|
||||
code: 'parse-error',
|
||||
message: err.message.replace(/ \(\d+:\d+\)$/, '')
|
||||
}, err.pos);
|
||||
}
|
||||
acorn_error(err: any) {
|
||||
this.error(
|
||||
{
|
||||
code: 'parse-error',
|
||||
message: err.message.replace(/ \(\d+:\d+\)$/, ''),
|
||||
},
|
||||
err.pos
|
||||
);
|
||||
}
|
||||
|
||||
error({ code, message }: { code: string; message: string }, index = this.index) {
|
||||
error(message, {
|
||||
name: 'ParseError',
|
||||
code,
|
||||
source: this.template,
|
||||
start: index,
|
||||
filename: this.filename
|
||||
});
|
||||
}
|
||||
error({ code, message }: { code: string; message: string }, index = this.index) {
|
||||
error(message, {
|
||||
name: 'ParseError',
|
||||
code,
|
||||
source: this.template,
|
||||
start: index,
|
||||
filename: this.filename,
|
||||
});
|
||||
}
|
||||
|
||||
eat(str: string, required?: boolean, message?: string) {
|
||||
if (this.match(str)) {
|
||||
this.index += str.length;
|
||||
return true;
|
||||
}
|
||||
eat(str: string, required?: boolean, message?: string) {
|
||||
if (this.match(str)) {
|
||||
this.index += str.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (required) {
|
||||
this.error({
|
||||
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
|
||||
message: message || `Expected ${str}`
|
||||
});
|
||||
}
|
||||
if (required) {
|
||||
this.error({
|
||||
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
|
||||
message: message || `Expected ${str}`,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
match(str: string) {
|
||||
return this.template.slice(this.index, this.index + str.length) === str;
|
||||
}
|
||||
match(str: string) {
|
||||
return this.template.slice(this.index, this.index + str.length) === str;
|
||||
}
|
||||
|
||||
match_regex(pattern: RegExp) {
|
||||
const match = pattern.exec(this.template.slice(this.index));
|
||||
if (!match || match.index !== 0) return null;
|
||||
match_regex(pattern: RegExp) {
|
||||
const match = pattern.exec(this.template.slice(this.index));
|
||||
if (!match || match.index !== 0) return null;
|
||||
|
||||
return match[0];
|
||||
}
|
||||
return match[0];
|
||||
}
|
||||
|
||||
allow_whitespace() {
|
||||
while (
|
||||
this.index < this.template.length &&
|
||||
whitespace.test(this.template[this.index])
|
||||
) {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
allow_whitespace() {
|
||||
while (this.index < this.template.length && whitespace.test(this.template[this.index])) {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
read(pattern: RegExp) {
|
||||
const result = this.match_regex(pattern);
|
||||
if (result) this.index += result.length;
|
||||
return result;
|
||||
}
|
||||
read(pattern: RegExp) {
|
||||
const result = this.match_regex(pattern);
|
||||
if (result) this.index += result.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
read_identifier(allow_reserved = false) {
|
||||
const start = this.index;
|
||||
read_identifier(allow_reserved = false) {
|
||||
const start = this.index;
|
||||
|
||||
let i = this.index;
|
||||
let i = this.index;
|
||||
|
||||
const code = full_char_code_at(this.template, i);
|
||||
if (!isIdentifierStart(code, true)) return null;
|
||||
const code = full_char_code_at(this.template, i);
|
||||
if (!isIdentifierStart(code, true)) return null;
|
||||
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (i < this.template.length) {
|
||||
const code = full_char_code_at(this.template, i);
|
||||
while (i < this.template.length) {
|
||||
const code = full_char_code_at(this.template, i);
|
||||
|
||||
if (!isIdentifierChar(code, true)) break;
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
if (!isIdentifierChar(code, true)) break;
|
||||
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)) {
|
||||
this.error({
|
||||
code: 'unexpected-reserved-word',
|
||||
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
|
||||
}, start);
|
||||
}
|
||||
if (!allow_reserved && reserved.has(identifier)) {
|
||||
this.error(
|
||||
{
|
||||
code: 'unexpected-reserved-word',
|
||||
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`,
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
read_until(pattern: RegExp) {
|
||||
if (this.index >= this.template.length) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
}
|
||||
read_until(pattern: RegExp) {
|
||||
if (this.index >= this.template.length) {
|
||||
this.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input',
|
||||
});
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const match = pattern.exec(this.template.slice(start));
|
||||
const start = this.index;
|
||||
const match = pattern.exec(this.template.slice(start));
|
||||
|
||||
if (match) {
|
||||
this.index = start + match.index;
|
||||
return this.template.slice(start, this.index);
|
||||
}
|
||||
if (match) {
|
||||
this.index = start + match.index;
|
||||
return this.template.slice(start, this.index);
|
||||
}
|
||||
|
||||
this.index = this.template.length;
|
||||
return this.template.slice(start);
|
||||
}
|
||||
this.index = this.template.length;
|
||||
return this.template.slice(start);
|
||||
}
|
||||
|
||||
require_whitespace() {
|
||||
if (!whitespace.test(this.template[this.index])) {
|
||||
this.error({
|
||||
code: 'missing-whitespace',
|
||||
message: 'Expected whitespace'
|
||||
});
|
||||
}
|
||||
require_whitespace() {
|
||||
if (!whitespace.test(this.template[this.index])) {
|
||||
this.error({
|
||||
code: 'missing-whitespace',
|
||||
message: 'Expected whitespace',
|
||||
});
|
||||
}
|
||||
|
||||
this.allow_whitespace();
|
||||
}
|
||||
this.allow_whitespace();
|
||||
}
|
||||
}
|
||||
|
||||
export default function parse(
|
||||
template: string,
|
||||
options: ParserOptions = {}
|
||||
): Ast {
|
||||
const parser = new Parser(template, options);
|
||||
export default function parse(template: string, options: ParserOptions = {}): Ast {
|
||||
const parser = new Parser(template, options);
|
||||
|
||||
// TODO we may want to allow multiple <style> tags —
|
||||
// one scoped, one global. for now, only allow one
|
||||
if (parser.css.length > 1) {
|
||||
parser.error({
|
||||
code: 'duplicate-style',
|
||||
message: 'You can only have one top-level <style> tag per component'
|
||||
}, parser.css[1].start);
|
||||
}
|
||||
// TODO we may want to allow multiple <style> tags —
|
||||
// one scoped, one global. for now, only allow one
|
||||
if (parser.css.length > 1) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'duplicate-style',
|
||||
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 module_scripts = parser.js.filter(script => script.context === 'module');
|
||||
const instance_scripts = parser.js.filter((script) => script.context === 'default');
|
||||
const module_scripts = parser.js.filter((script) => script.context === 'module');
|
||||
|
||||
if (instance_scripts.length > 1) {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one instance-level <script> element'
|
||||
}, instance_scripts[1].start);
|
||||
}
|
||||
if (instance_scripts.length > 1) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one instance-level <script> element',
|
||||
},
|
||||
instance_scripts[1].start
|
||||
);
|
||||
}
|
||||
|
||||
if (module_scripts.length > 1) {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one <script context="module"> element'
|
||||
}, module_scripts[1].start);
|
||||
}
|
||||
if (module_scripts.length > 1) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-script',
|
||||
message: 'A component can only have one <script context="module"> element',
|
||||
},
|
||||
module_scripts[1].start
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
html: parser.html,
|
||||
css: parser.css[0],
|
||||
instance: instance_scripts[0],
|
||||
module: module_scripts[0]
|
||||
};
|
||||
return {
|
||||
html: parser.html,
|
||||
css: parser.css[0],
|
||||
instance: instance_scripts[0],
|
||||
module: module_scripts[0],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,82 +3,70 @@
|
|||
import { Parser } from '../index.js';
|
||||
import { isIdentifierStart } from 'acorn';
|
||||
import full_char_code_at from '../../utils/full_char_code_at.js';
|
||||
import {
|
||||
is_bracket_open,
|
||||
is_bracket_close,
|
||||
is_bracket_pair,
|
||||
get_bracket_close
|
||||
} from '../utils/bracket.js';
|
||||
import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js';
|
||||
import { parse_expression_at } from '../acorn.js';
|
||||
import { Pattern } from 'estree';
|
||||
|
||||
export default function read_context(
|
||||
parser: Parser
|
||||
): Pattern & { start: number; end: number } {
|
||||
const start = parser.index;
|
||||
let i = parser.index;
|
||||
export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
|
||||
const start = parser.index;
|
||||
let i = parser.index;
|
||||
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (isIdentifierStart(code, true)) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name: parser.read_identifier(),
|
||||
start,
|
||||
end: parser.index
|
||||
};
|
||||
}
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (isIdentifierStart(code, true)) {
|
||||
return {
|
||||
type: 'Identifier',
|
||||
name: parser.read_identifier(),
|
||||
start,
|
||||
end: parser.index,
|
||||
};
|
||||
}
|
||||
|
||||
if (!is_bracket_open(code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected identifier or destructure pattern'
|
||||
});
|
||||
}
|
||||
if (!is_bracket_open(code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected identifier or destructure pattern',
|
||||
});
|
||||
}
|
||||
|
||||
const bracket_stack = [code];
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
const bracket_stack = [code];
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
|
||||
while (i < parser.template.length) {
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (is_bracket_open(code)) {
|
||||
bracket_stack.push(code);
|
||||
} else if (is_bracket_close(code)) {
|
||||
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: `Expected ${String.fromCharCode(
|
||||
get_bracket_close(bracket_stack[bracket_stack.length - 1])
|
||||
)}`
|
||||
});
|
||||
}
|
||||
bracket_stack.pop();
|
||||
if (bracket_stack.length === 0) {
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
while (i < parser.template.length) {
|
||||
const code = full_char_code_at(parser.template, i);
|
||||
if (is_bracket_open(code)) {
|
||||
bracket_stack.push(code);
|
||||
} else if (is_bracket_close(code)) {
|
||||
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`,
|
||||
});
|
||||
}
|
||||
bracket_stack.pop();
|
||||
if (bracket_stack.length === 0) {
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += code <= 0xffff ? 1 : 2;
|
||||
}
|
||||
|
||||
parser.index = i;
|
||||
parser.index = i;
|
||||
|
||||
const pattern_string = parser.template.slice(start, i);
|
||||
try {
|
||||
// the length of the `space_with_newline` has to be start - 1
|
||||
// because we added a `(` in front of the pattern_string,
|
||||
// which shifted the entire string to right by 1
|
||||
// so we offset it by removing 1 character in the `space_with_newline`
|
||||
// to achieve that, we remove the 1st space encountered,
|
||||
// so it will not affect the `column` of the node
|
||||
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
|
||||
const first_space = space_with_newline.indexOf(' ');
|
||||
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
||||
const pattern_string = parser.template.slice(start, i);
|
||||
try {
|
||||
// the length of the `space_with_newline` has to be start - 1
|
||||
// because we added a `(` in front of the pattern_string,
|
||||
// which shifted the entire string to right by 1
|
||||
// so we offset it by removing 1 character in the `space_with_newline`
|
||||
// to achieve that, we remove the 1st space encountered,
|
||||
// so it will not affect the `column` of the node
|
||||
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
|
||||
const first_space = space_with_newline.indexOf(' ');
|
||||
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
|
||||
|
||||
return (parse_expression_at(
|
||||
`${space_with_newline}(${pattern_string} = 1)`,
|
||||
start - 1
|
||||
) as any).left;
|
||||
} catch (error) {
|
||||
parser.acorn_error(error);
|
||||
}
|
||||
return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left;
|
||||
} catch (error) {
|
||||
parser.acorn_error(error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,36 +6,39 @@ import { whitespace } from '../../utils/patterns.js';
|
|||
// import { Node } from 'estree';
|
||||
|
||||
export default function read_expression(parser: Parser): string {
|
||||
try {
|
||||
const node = parse_expression_at(parser.template, parser.index);
|
||||
try {
|
||||
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) {
|
||||
if (parser.template[i] === '(') num_parens += 1;
|
||||
}
|
||||
for (let i = parser.index; i < node.start; i += 1) {
|
||||
if (parser.template[i] === '(') num_parens += 1;
|
||||
}
|
||||
|
||||
let index = node.end;
|
||||
while (num_parens > 0) {
|
||||
const char = parser.template[index];
|
||||
let index = node.end;
|
||||
while (num_parens > 0) {
|
||||
const char = parser.template[index];
|
||||
|
||||
if (char === ')') {
|
||||
num_parens -= 1;
|
||||
} else if (!whitespace.test(char)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected )'
|
||||
}, index);
|
||||
}
|
||||
if (char === ')') {
|
||||
num_parens -= 1;
|
||||
} else if (!whitespace.test(char)) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected )',
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
parser.index = index;
|
||||
parser.index = index;
|
||||
|
||||
return parser.template.substring(node.start, node.end);
|
||||
// return node as Node;
|
||||
} catch (err) {
|
||||
parser.acorn_error(err);
|
||||
}
|
||||
return parser.template.substring(node.start, node.end);
|
||||
// return node as Node;
|
||||
} catch (err) {
|
||||
parser.acorn_error(err);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,48 +8,53 @@ import { Node, Program } from 'estree';
|
|||
const script_closing_tag = '</script>';
|
||||
|
||||
function get_context(parser: Parser, attributes: any[], start: number): string {
|
||||
const context = attributes.find(attribute => attribute.name === 'context');
|
||||
if (!context) return 'default';
|
||||
const context = attributes.find((attribute) => attribute.name === 'context');
|
||||
if (!context) return 'default';
|
||||
|
||||
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'context attribute must be static'
|
||||
}, start);
|
||||
}
|
||||
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-script',
|
||||
message: 'context attribute must be static',
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
const value = context.value[0].data;
|
||||
const value = context.value[0].data;
|
||||
|
||||
if (value !== 'module') {
|
||||
parser.error({
|
||||
code: 'invalid-script',
|
||||
message: 'If the context attribute is supplied, its value must be "module"'
|
||||
}, context.start);
|
||||
}
|
||||
if (value !== 'module') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-script',
|
||||
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 {
|
||||
const script_start = parser.index;
|
||||
const script_end = parser.template.indexOf(script_closing_tag, script_start);
|
||||
const script_start = parser.index;
|
||||
const script_end = parser.template.indexOf(script_closing_tag, script_start);
|
||||
|
||||
if (script_end === -1) {
|
||||
parser.error({
|
||||
code: 'unclosed-script',
|
||||
message: '<script> must have a closing tag'
|
||||
});
|
||||
}
|
||||
if (script_end === -1) {
|
||||
parser.error({
|
||||
code: 'unclosed-script',
|
||||
message: '<script> must have a closing tag',
|
||||
});
|
||||
}
|
||||
|
||||
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
|
||||
parser.template.slice(script_start, script_end);
|
||||
parser.index = script_end + script_closing_tag.length;
|
||||
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + parser.template.slice(script_start, script_end);
|
||||
parser.index = script_end + script_closing_tag.length;
|
||||
|
||||
return {
|
||||
type: 'Script',
|
||||
start,
|
||||
end: parser.index,
|
||||
context: get_context(parser, attributes, start),
|
||||
content: source
|
||||
};
|
||||
return {
|
||||
type: 'Script',
|
||||
start,
|
||||
end: parser.index,
|
||||
context: get_context(parser, attributes, start),
|
||||
content: source,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import text from './text.js';
|
|||
import { Parser } from '../index.js';
|
||||
|
||||
export default function fragment(parser: Parser) {
|
||||
if (parser.match('<')) {
|
||||
return tag;
|
||||
}
|
||||
if (parser.match('<')) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (parser.match('{')) {
|
||||
return mustache;
|
||||
}
|
||||
if (parser.match('{')) {
|
||||
return mustache;
|
||||
}
|
||||
|
||||
return text;
|
||||
return text;
|
||||
}
|
||||
|
|
|
@ -10,403 +10,404 @@ import { TemplateNode } from '../../interfaces.js';
|
|||
type TODO = any;
|
||||
|
||||
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 last_child = block.children[block.children.length - 1];
|
||||
const first_child = block.children[0];
|
||||
const last_child = block.children[block.children.length - 1];
|
||||
|
||||
if (first_child.type === 'Text' && trim_before) {
|
||||
first_child.data = trim_start(first_child.data);
|
||||
if (!first_child.data) block.children.shift();
|
||||
}
|
||||
if (first_child.type === 'Text' && trim_before) {
|
||||
first_child.data = trim_start(first_child.data);
|
||||
if (!first_child.data) block.children.shift();
|
||||
}
|
||||
|
||||
if (last_child.type === 'Text' && trim_after) {
|
||||
last_child.data = trim_end(last_child.data);
|
||||
if (!last_child.data) block.children.pop();
|
||||
}
|
||||
if (last_child.type === 'Text' && trim_after) {
|
||||
last_child.data = trim_end(last_child.data);
|
||||
if (!last_child.data) block.children.pop();
|
||||
}
|
||||
|
||||
if (block.else) {
|
||||
trim_whitespace(block.else, trim_before, trim_after);
|
||||
}
|
||||
if (block.else) {
|
||||
trim_whitespace(block.else, trim_before, trim_after);
|
||||
}
|
||||
|
||||
if (first_child.elseif) {
|
||||
trim_whitespace(first_child, trim_before, trim_after);
|
||||
}
|
||||
if (first_child.elseif) {
|
||||
trim_whitespace(first_child, trim_before, trim_after);
|
||||
}
|
||||
}
|
||||
|
||||
export default function mustache(parser: Parser) {
|
||||
const start = parser.index;
|
||||
parser.index += 1;
|
||||
const start = parser.index;
|
||||
parser.index += 1;
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {/if}, {/each}, {/await} or {/key}
|
||||
if (parser.eat('/')) {
|
||||
let block = parser.current();
|
||||
let expected: TODO;
|
||||
// {/if}, {/each}, {/await} or {/key}
|
||||
if (parser.eat('/')) {
|
||||
let block = parser.current();
|
||||
let expected: TODO;
|
||||
|
||||
if (closing_tag_omitted(block.name)) {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
}
|
||||
if (closing_tag_omitted(block.name)) {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
}
|
||||
|
||||
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
|
||||
expected = 'await';
|
||||
}
|
||||
expected = 'await';
|
||||
}
|
||||
|
||||
if (block.type === 'IfBlock') {
|
||||
expected = 'if';
|
||||
} else if (block.type === 'EachBlock') {
|
||||
expected = 'each';
|
||||
} else if (block.type === 'AwaitBlock') {
|
||||
expected = 'await';
|
||||
} else if (block.type === 'KeyBlock') {
|
||||
expected = 'key';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'unexpected-block-close',
|
||||
message: 'Unexpected block closing tag'
|
||||
});
|
||||
}
|
||||
if (block.type === 'IfBlock') {
|
||||
expected = 'if';
|
||||
} else if (block.type === 'EachBlock') {
|
||||
expected = 'each';
|
||||
} else if (block.type === 'AwaitBlock') {
|
||||
expected = 'await';
|
||||
} else if (block.type === 'KeyBlock') {
|
||||
expected = 'key';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'unexpected-block-close',
|
||||
message: 'Unexpected block closing tag',
|
||||
});
|
||||
}
|
||||
|
||||
parser.eat(expected, true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.eat(expected, true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
while (block.elseif) {
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
while (block.elseif) {
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
block = parser.current();
|
||||
|
||||
if (block.else) {
|
||||
block.else.end = start;
|
||||
}
|
||||
}
|
||||
if (block.else) {
|
||||
block.else.end = start;
|
||||
}
|
||||
}
|
||||
|
||||
// strip leading/trailing whitespace as necessary
|
||||
const char_before = parser.template[block.start - 1];
|
||||
const char_after = parser.template[parser.index];
|
||||
const trim_before = !char_before || whitespace.test(char_before);
|
||||
const trim_after = !char_after || whitespace.test(char_after);
|
||||
// strip leading/trailing whitespace as necessary
|
||||
const char_before = parser.template[block.start - 1];
|
||||
const char_after = parser.template[parser.index];
|
||||
const trim_before = !char_before || whitespace.test(char_before);
|
||||
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;
|
||||
parser.stack.pop();
|
||||
} else if (parser.eat(':else')) {
|
||||
if (parser.eat('if')) {
|
||||
parser.error({
|
||||
code: 'invalid-elseif',
|
||||
message: "'elseif' should be 'else if'"
|
||||
});
|
||||
}
|
||||
block.end = parser.index;
|
||||
parser.stack.pop();
|
||||
} else if (parser.eat(':else')) {
|
||||
if (parser.eat('if')) {
|
||||
parser.error({
|
||||
code: 'invalid-elseif',
|
||||
message: "'elseif' should be 'else if'",
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
// :else if
|
||||
if (parser.eat('if')) {
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-elseif-placement',
|
||||
message: parser.stack.some(block => block.type === 'IfBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
|
||||
: 'Cannot have an {:else if ...} block outside an {#if ...} block'
|
||||
});
|
||||
}
|
||||
// :else if
|
||||
if (parser.eat('if')) {
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-elseif-placement',
|
||||
message: parser.stack.some((block) => block.type === 'IfBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else 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.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: [
|
||||
{
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'IfBlock',
|
||||
elseif: true,
|
||||
expression,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
};
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: [
|
||||
{
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'IfBlock',
|
||||
elseif: true,
|
||||
expression,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
parser.stack.push(block.else.children[0]);
|
||||
} else {
|
||||
// :else
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-else-placement',
|
||||
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else} block`
|
||||
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block'
|
||||
});
|
||||
}
|
||||
parser.stack.push(block.else.children[0]);
|
||||
} else {
|
||||
// :else
|
||||
const block = parser.current();
|
||||
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-else-placement',
|
||||
message: parser.stack.some((block) => block.type === 'IfBlock' || block.type === 'EachBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:else} block`
|
||||
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block',
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: []
|
||||
};
|
||||
block.else = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'ElseBlock',
|
||||
children: [],
|
||||
};
|
||||
|
||||
parser.stack.push(block.else);
|
||||
}
|
||||
} else if (parser.match(':then') || parser.match(':catch')) {
|
||||
const block = parser.current();
|
||||
const is_then = parser.eat(':then') || !parser.eat(':catch');
|
||||
parser.stack.push(block.else);
|
||||
}
|
||||
} else if (parser.match(':then') || parser.match(':catch')) {
|
||||
const block = parser.current();
|
||||
const is_then = parser.eat(':then') || !parser.eat(':catch');
|
||||
|
||||
if (is_then) {
|
||||
if (block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-then-placement',
|
||||
message: parser.stack.some(block => block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:then} block`
|
||||
: 'Cannot have an {:then} block outside an {#await ...} block'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-catch-placement',
|
||||
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:catch} block`
|
||||
: 'Cannot have an {:catch} block outside an {#await ...} block'
|
||||
});
|
||||
}
|
||||
}
|
||||
if (is_then) {
|
||||
if (block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-then-placement',
|
||||
message: parser.stack.some((block) => block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:then} block`
|
||||
: 'Cannot have an {:then} block outside an {#await ...} block',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
|
||||
parser.error({
|
||||
code: 'invalid-catch-placement',
|
||||
message: parser.stack.some((block) => block.type === 'ThenBlock' || block.type === 'PendingBlock')
|
||||
? `Expected to close ${to_string(block)} before seeing {:catch} block`
|
||||
: 'Cannot have an {:catch} block outside an {#await ...} block',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
const await_block = parser.current();
|
||||
block.end = start;
|
||||
parser.stack.pop();
|
||||
const await_block = parser.current();
|
||||
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
await_block[is_then ? 'value' : 'error'] = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
if (!parser.eat('}')) {
|
||||
parser.require_whitespace();
|
||||
await_block[is_then ? 'value' : 'error'] = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
}
|
||||
|
||||
const new_block: TemplateNode = {
|
||||
start,
|
||||
// @ts-ignore
|
||||
end: null,
|
||||
type: is_then ? 'ThenBlock' : 'CatchBlock',
|
||||
children: [],
|
||||
skip: false
|
||||
};
|
||||
const new_block: TemplateNode = {
|
||||
start,
|
||||
// @ts-ignore
|
||||
end: null,
|
||||
type: is_then ? 'ThenBlock' : 'CatchBlock',
|
||||
children: [],
|
||||
skip: false,
|
||||
};
|
||||
|
||||
await_block[is_then ? 'then' : 'catch'] = new_block;
|
||||
parser.stack.push(new_block);
|
||||
} else if (parser.eat('#')) {
|
||||
// {#if foo}, {#each foo} or {#await foo}
|
||||
let type;
|
||||
await_block[is_then ? 'then' : 'catch'] = new_block;
|
||||
parser.stack.push(new_block);
|
||||
} else if (parser.eat('#')) {
|
||||
// {#if foo}, {#each foo} or {#await foo}
|
||||
let type;
|
||||
|
||||
if (parser.eat('if')) {
|
||||
type = 'IfBlock';
|
||||
} else if (parser.eat('each')) {
|
||||
type = 'EachBlock';
|
||||
} else if (parser.eat('await')) {
|
||||
type = 'AwaitBlock';
|
||||
} else if (parser.eat('key')) {
|
||||
type = 'KeyBlock';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'expected-block-type',
|
||||
message: 'Expected if, each, await or key'
|
||||
});
|
||||
}
|
||||
if (parser.eat('if')) {
|
||||
type = 'IfBlock';
|
||||
} else if (parser.eat('each')) {
|
||||
type = 'EachBlock';
|
||||
} else if (parser.eat('await')) {
|
||||
type = 'AwaitBlock';
|
||||
} else if (parser.eat('key')) {
|
||||
type = 'KeyBlock';
|
||||
} else {
|
||||
parser.error({
|
||||
code: 'expected-block-type',
|
||||
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
|
||||
const block: TemplateNode = type === 'AwaitBlock' ?
|
||||
{
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
value: null,
|
||||
error: null,
|
||||
pending: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'PendingBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
},
|
||||
then: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'ThenBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
},
|
||||
catch: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'CatchBlock',
|
||||
children: [],
|
||||
skip: true
|
||||
}
|
||||
} :
|
||||
{
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
children: []
|
||||
};
|
||||
// @ts-ignore
|
||||
const block: TemplateNode =
|
||||
type === 'AwaitBlock'
|
||||
? {
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
value: null,
|
||||
error: null,
|
||||
pending: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'PendingBlock',
|
||||
children: [],
|
||||
skip: true,
|
||||
},
|
||||
then: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'ThenBlock',
|
||||
children: [],
|
||||
skip: true,
|
||||
},
|
||||
catch: {
|
||||
start: null,
|
||||
end: null,
|
||||
type: 'CatchBlock',
|
||||
children: [],
|
||||
skip: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
start,
|
||||
end: null,
|
||||
type,
|
||||
expression,
|
||||
children: [],
|
||||
};
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
// {#each} blocks must declare a context – {#each list as item}
|
||||
if (type === 'EachBlock') {
|
||||
parser.eat('as', true);
|
||||
parser.require_whitespace();
|
||||
// {#each} blocks must declare a context – {#each list as item}
|
||||
if (type === 'EachBlock') {
|
||||
parser.eat('as', true);
|
||||
parser.require_whitespace();
|
||||
|
||||
block.context = read_context(parser);
|
||||
block.context = read_context(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
block.index = parser.read_identifier();
|
||||
if (!block.index) {
|
||||
parser.error({
|
||||
code: 'expected-name',
|
||||
message: 'Expected name'
|
||||
});
|
||||
}
|
||||
if (parser.eat(',')) {
|
||||
parser.allow_whitespace();
|
||||
block.index = parser.read_identifier();
|
||||
if (!block.index) {
|
||||
parser.error({
|
||||
code: 'expected-name',
|
||||
message: 'Expected name',
|
||||
});
|
||||
}
|
||||
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (parser.eat('(')) {
|
||||
parser.allow_whitespace();
|
||||
if (parser.eat('(')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
block.key = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(')', true);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
block.key = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat(')', true);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
}
|
||||
|
||||
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
|
||||
if (await_block_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.value = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
|
||||
if (await_block_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.value = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
|
||||
if (await_block_catch_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.error = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
|
||||
if (await_block_catch_shorthand) {
|
||||
parser.require_whitespace();
|
||||
block.error = read_context(parser);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
parser.eat('}', true);
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push(block);
|
||||
parser.stack.push(block);
|
||||
// @ts-ignore
|
||||
parser.current().children.push(block);
|
||||
parser.stack.push(block);
|
||||
|
||||
if (type === 'AwaitBlock') {
|
||||
let child_block;
|
||||
if (await_block_shorthand) {
|
||||
block.then.skip = false;
|
||||
child_block = block.then;
|
||||
} else if (await_block_catch_shorthand) {
|
||||
block.catch.skip = false;
|
||||
child_block = block.catch;
|
||||
} else {
|
||||
block.pending.skip = false;
|
||||
child_block = block.pending;
|
||||
}
|
||||
if (type === 'AwaitBlock') {
|
||||
let child_block;
|
||||
if (await_block_shorthand) {
|
||||
block.then.skip = false;
|
||||
child_block = block.then;
|
||||
} else if (await_block_catch_shorthand) {
|
||||
block.catch.skip = false;
|
||||
child_block = block.catch;
|
||||
} else {
|
||||
block.pending.skip = false;
|
||||
child_block = block.pending;
|
||||
}
|
||||
|
||||
child_block.start = parser.index;
|
||||
parser.stack.push(child_block);
|
||||
}
|
||||
} else if (parser.eat('@html')) {
|
||||
// {@html content} tag
|
||||
parser.require_whitespace();
|
||||
child_block.start = parser.index;
|
||||
parser.stack.push(child_block);
|
||||
}
|
||||
} else if (parser.eat('@html')) {
|
||||
// {@html content} tag
|
||||
parser.require_whitespace();
|
||||
|
||||
const expression = read_expression(parser);
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'RawMustacheTag',
|
||||
expression
|
||||
});
|
||||
} else if (parser.eat('@debug')) {
|
||||
// let identifiers;
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'RawMustacheTag',
|
||||
expression,
|
||||
});
|
||||
} else if (parser.eat('@debug')) {
|
||||
// let identifiers;
|
||||
|
||||
// // Implies {@debug} which indicates "debug all"
|
||||
// if (parser.read(/\s*}/)) {
|
||||
// identifiers = [];
|
||||
// } else {
|
||||
// const expression = read_expression(parser);
|
||||
// // Implies {@debug} which indicates "debug all"
|
||||
// if (parser.read(/\s*}/)) {
|
||||
// identifiers = [];
|
||||
// } else {
|
||||
// const expression = read_expression(parser);
|
||||
|
||||
// identifiers = expression.type === 'SequenceExpression'
|
||||
// ? expression.expressions
|
||||
// : [expression];
|
||||
// identifiers = expression.type === 'SequenceExpression'
|
||||
// ? expression.expressions
|
||||
// : [expression];
|
||||
|
||||
// identifiers.forEach(node => {
|
||||
// if (node.type !== 'Identifier') {
|
||||
// parser.error({
|
||||
// code: 'invalid-debug-args',
|
||||
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
|
||||
// }, node.start);
|
||||
// }
|
||||
// });
|
||||
// identifiers.forEach(node => {
|
||||
// if (node.type !== 'Identifier') {
|
||||
// parser.error({
|
||||
// code: 'invalid-debug-args',
|
||||
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
|
||||
// }, node.start);
|
||||
// }
|
||||
// });
|
||||
|
||||
// parser.allow_whitespace();
|
||||
// parser.eat('}', true);
|
||||
// }
|
||||
// parser.allow_whitespace();
|
||||
// parser.eat('}', true);
|
||||
// }
|
||||
|
||||
// parser.current().children.push({
|
||||
// start,
|
||||
// end: parser.index,
|
||||
// type: 'DebugTag',
|
||||
// identifiers
|
||||
// });
|
||||
throw new Error('@debug not yet supported');
|
||||
} else {
|
||||
const expression = read_expression(parser);
|
||||
// parser.current().children.push({
|
||||
// start,
|
||||
// end: parser.index,
|
||||
// type: 'DebugTag',
|
||||
// identifiers
|
||||
// });
|
||||
throw new Error('@debug not yet supported');
|
||||
} else {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,29 +14,29 @@ import list from '../../utils/list.js';
|
|||
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
|
||||
|
||||
const meta_tags = new Map([
|
||||
['svelte:head', 'Head'],
|
||||
['svelte:options', 'Options'],
|
||||
['svelte:window', 'Window'],
|
||||
['svelte:body', 'Body']
|
||||
['svelte:head', 'Head'],
|
||||
['svelte:options', 'Options'],
|
||||
['svelte:window', 'Window'],
|
||||
['svelte:body', 'Body'],
|
||||
]);
|
||||
|
||||
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
|
||||
|
||||
const specials = new Map([
|
||||
[
|
||||
'script',
|
||||
{
|
||||
read: read_script,
|
||||
property: 'js'
|
||||
}
|
||||
],
|
||||
[
|
||||
'style',
|
||||
{
|
||||
read: read_style,
|
||||
property: 'css'
|
||||
}
|
||||
]
|
||||
[
|
||||
'script',
|
||||
{
|
||||
read: read_script,
|
||||
property: 'js',
|
||||
},
|
||||
],
|
||||
[
|
||||
'style',
|
||||
{
|
||||
read: read_style,
|
||||
property: 'css',
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const SELF = /^svelte:self(?=[\s/>])/;
|
||||
|
@ -44,489 +44,531 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
|
|||
const SLOT = /^svelte:fragment(?=[\s/>])/;
|
||||
|
||||
function parent_is_head(stack) {
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'Head') return true;
|
||||
if (type === 'Element' || type === 'InlineComponent') return false;
|
||||
}
|
||||
return false;
|
||||
let i = stack.length;
|
||||
while (i--) {
|
||||
const { type } = stack[i];
|
||||
if (type === 'Head') return true;
|
||||
if (type === 'Element' || type === 'InlineComponent') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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('!--')) {
|
||||
const data = parser.read_until(/-->/);
|
||||
parser.eat('-->', true, 'comment was left open, expected -->');
|
||||
if (parser.eat('!--')) {
|
||||
const data = parser.read_until(/-->/);
|
||||
parser.eat('-->', true, 'comment was left open, expected -->');
|
||||
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Comment',
|
||||
data
|
||||
});
|
||||
parser.current().children.push({
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Comment',
|
||||
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)) {
|
||||
const slug = meta_tags.get(name).toLowerCase();
|
||||
if (is_closing_tag) {
|
||||
if (
|
||||
(name === 'svelte:window' || name === 'svelte:body') &&
|
||||
parser.current().children.length
|
||||
) {
|
||||
parser.error({
|
||||
code: `invalid-${slug}-content`,
|
||||
message: `<${name}> cannot have children`
|
||||
}, parser.current().children[0].start);
|
||||
}
|
||||
} else {
|
||||
if (name in parser.meta_tags) {
|
||||
parser.error({
|
||||
code: `duplicate-${slug}`,
|
||||
message: `A component can only have one <${name}> tag`
|
||||
}, start);
|
||||
}
|
||||
if (meta_tags.has(name)) {
|
||||
const slug = meta_tags.get(name).toLowerCase();
|
||||
if (is_closing_tag) {
|
||||
if ((name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length) {
|
||||
parser.error(
|
||||
{
|
||||
code: `invalid-${slug}-content`,
|
||||
message: `<${name}> cannot have children`,
|
||||
},
|
||||
parser.current().children[0].start
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (name in parser.meta_tags) {
|
||||
parser.error(
|
||||
{
|
||||
code: `duplicate-${slug}`,
|
||||
message: `A component can only have one <${name}> tag`,
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
if (parser.stack.length > 1) {
|
||||
parser.error({
|
||||
code: `invalid-${slug}-placement`,
|
||||
message: `<${name}> tags cannot be inside elements or blocks`
|
||||
}, start);
|
||||
}
|
||||
if (parser.stack.length > 1) {
|
||||
parser.error(
|
||||
{
|
||||
code: `invalid-${slug}-placement`,
|
||||
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)
|
||||
? meta_tags.get(name)
|
||||
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
|
||||
: name === 'svelte:fragment' ? 'SlotTemplate'
|
||||
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
|
||||
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
|
||||
const type = meta_tags.has(name)
|
||||
? meta_tags.get(name)
|
||||
: /[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
|
||||
? 'InlineComponent'
|
||||
: name === 'svelte:fragment'
|
||||
? 'SlotTemplate'
|
||||
: name === 'title' && parent_is_head(parser.stack)
|
||||
? 'Title'
|
||||
: name === 'slot' && !parser.customElement
|
||||
? 'Slot'
|
||||
: 'Element';
|
||||
|
||||
const element: TemplateNode = {
|
||||
start,
|
||||
end: null, // filled in later
|
||||
type,
|
||||
name,
|
||||
attributes: [],
|
||||
children: []
|
||||
};
|
||||
const element: TemplateNode = {
|
||||
start,
|
||||
end: null, // filled in later
|
||||
type,
|
||||
name,
|
||||
attributes: [],
|
||||
children: [],
|
||||
};
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (is_closing_tag) {
|
||||
if (is_void(name)) {
|
||||
parser.error({
|
||||
code: 'invalid-void-content',
|
||||
message: `<${name}> is a void element and cannot have children, or a closing tag`
|
||||
}, start);
|
||||
}
|
||||
if (is_closing_tag) {
|
||||
if (is_void(name)) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-void-content',
|
||||
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>
|
||||
while (parent.name !== name) {
|
||||
if (parent.type !== 'Element') {
|
||||
const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
|
||||
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
|
||||
: `</${name}> attempted to close an element that was not open`;
|
||||
parser.error({
|
||||
code: 'invalid-closing-tag',
|
||||
message
|
||||
}, start);
|
||||
}
|
||||
// close any elements that don't have their own closing tags, e.g. <div><p></div>
|
||||
while (parent.name !== name) {
|
||||
if (parent.type !== 'Element') {
|
||||
const message =
|
||||
parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
|
||||
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
|
||||
: `</${name}> attempted to close an element that was not open`;
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-closing-tag',
|
||||
message,
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
|
||||
parent = parser.current();
|
||||
}
|
||||
parent = parser.current();
|
||||
}
|
||||
|
||||
parent.end = parser.index;
|
||||
parser.stack.pop();
|
||||
parent.end = parser.index;
|
||||
parser.stack.pop();
|
||||
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = null;
|
||||
}
|
||||
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
|
||||
parser.last_auto_closed_tag = null;
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (closing_tag_omitted(parent.name, name)) {
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: name,
|
||||
depth: parser.stack.length
|
||||
};
|
||||
}
|
||||
return;
|
||||
} else if (closing_tag_omitted(parent.name, name)) {
|
||||
parent.end = start;
|
||||
parser.stack.pop();
|
||||
parser.last_auto_closed_tag = {
|
||||
tag: parent.name,
|
||||
reason: name,
|
||||
depth: parser.stack.length,
|
||||
};
|
||||
}
|
||||
|
||||
const unique_names: Set<string> = new Set();
|
||||
const unique_names: Set<string> = new Set();
|
||||
|
||||
let attribute;
|
||||
while ((attribute = read_attribute(parser, unique_names))) {
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
let attribute;
|
||||
while ((attribute = read_attribute(parser, unique_names))) {
|
||||
element.attributes.push(attribute);
|
||||
parser.allow_whitespace();
|
||||
}
|
||||
|
||||
if (name === 'svelte:component') {
|
||||
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
|
||||
if (!~index) {
|
||||
parser.error({
|
||||
code: 'missing-component-definition',
|
||||
message: "<svelte:component> must have a 'this' attribute"
|
||||
}, start);
|
||||
}
|
||||
if (name === 'svelte:component') {
|
||||
const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
|
||||
if (!~index) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'missing-component-definition',
|
||||
message: "<svelte:component> must have a 'this' attribute",
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
const definition = element.attributes.splice(index, 1)[0];
|
||||
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-component-definition',
|
||||
message: 'invalid component definition'
|
||||
}, definition.start);
|
||||
}
|
||||
const definition = element.attributes.splice(index, 1)[0];
|
||||
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-component-definition',
|
||||
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>
|
||||
if (specials.has(name) && parser.stack.length === 1) {
|
||||
const special = specials.get(name);
|
||||
// special cases – top-level <script> and <style>
|
||||
if (specials.has(name) && parser.stack.length === 1) {
|
||||
const special = specials.get(name);
|
||||
|
||||
parser.eat('>', true);
|
||||
const content = special.read(parser, start, element.attributes);
|
||||
if (content) parser[special.property].push(content);
|
||||
return;
|
||||
}
|
||||
parser.eat('>', true);
|
||||
const content = special.read(parser, start, element.attributes);
|
||||
if (content) parser[special.property].push(content);
|
||||
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) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (name === 'textarea') {
|
||||
// special case
|
||||
element.children = read_sequence(
|
||||
parser,
|
||||
() =>
|
||||
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
|
||||
);
|
||||
parser.read(/<\/textarea>/);
|
||||
element.end = parser.index;
|
||||
} else if (name === 'script' || name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||
const end = parser.index;
|
||||
element.children.push({ start, end, type: 'Text', data });
|
||||
parser.eat(`</${name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
}
|
||||
if (self_closing) {
|
||||
// don't push self-closing elements onto the stack
|
||||
element.end = parser.index;
|
||||
} else if (name === 'textarea') {
|
||||
// special case
|
||||
element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>');
|
||||
parser.read(/<\/textarea>/);
|
||||
element.end = parser.index;
|
||||
} else if (name === 'script' || name === 'style') {
|
||||
// special case
|
||||
const start = parser.index;
|
||||
const data = parser.read_until(new RegExp(`</${name}>`));
|
||||
const end = parser.index;
|
||||
element.children.push({ start, end, type: 'Text', data });
|
||||
parser.eat(`</${name}>`, true);
|
||||
element.end = parser.index;
|
||||
} else {
|
||||
parser.stack.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
function read_tag_name(parser: Parser) {
|
||||
const start = parser.index;
|
||||
const start = parser.index;
|
||||
|
||||
if (parser.read(SELF)) {
|
||||
// check we're inside a block, otherwise this
|
||||
// will cause infinite recursion
|
||||
let i = parser.stack.length;
|
||||
let legal = false;
|
||||
if (parser.read(SELF)) {
|
||||
// check we're inside a block, otherwise this
|
||||
// will cause infinite recursion
|
||||
let i = parser.stack.length;
|
||||
let legal = false;
|
||||
|
||||
while (i--) {
|
||||
const fragment = parser.stack[i];
|
||||
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
|
||||
legal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (i--) {
|
||||
const fragment = parser.stack[i];
|
||||
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
|
||||
legal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!legal) {
|
||||
parser.error({
|
||||
code: 'invalid-self-placement',
|
||||
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components'
|
||||
}, start);
|
||||
}
|
||||
if (!legal) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-self-placement',
|
||||
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:')) {
|
||||
const match = fuzzymatch(name.slice(7), valid_meta_tags);
|
||||
if (name.startsWith('svelte:')) {
|
||||
const match = fuzzymatch(name.slice(7), valid_meta_tags);
|
||||
|
||||
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
|
||||
if (match) message += ` (did you mean '${match}'?)`;
|
||||
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
|
||||
if (match) message += ` (did you mean '${match}'?)`;
|
||||
|
||||
parser.error({
|
||||
code: 'invalid-tag-name',
|
||||
message
|
||||
}, start);
|
||||
}
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-tag-name',
|
||||
message,
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
if (!valid_tag_name.test(name)) {
|
||||
parser.error({
|
||||
code: 'invalid-tag-name',
|
||||
message: 'Expected valid tag name'
|
||||
}, start);
|
||||
}
|
||||
if (!valid_tag_name.test(name)) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-tag-name',
|
||||
message: 'Expected valid tag name',
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
return name;
|
||||
return name;
|
||||
}
|
||||
|
||||
function read_attribute(parser: Parser, unique_names: Set<string>) {
|
||||
const start = parser.index;
|
||||
const start = parser.index;
|
||||
|
||||
function check_unique(name: string) {
|
||||
if (unique_names.has(name)) {
|
||||
parser.error({
|
||||
code: 'duplicate-attribute',
|
||||
message: 'Attributes need to be unique'
|
||||
}, start);
|
||||
}
|
||||
unique_names.add(name);
|
||||
}
|
||||
function check_unique(name: string) {
|
||||
if (unique_names.has(name)) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'duplicate-attribute',
|
||||
message: 'Attributes need to be unique',
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
unique_names.add(name);
|
||||
}
|
||||
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
if (parser.eat('{')) {
|
||||
parser.allow_whitespace();
|
||||
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
if (parser.eat('...')) {
|
||||
const expression = read_expression(parser);
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Spread',
|
||||
expression
|
||||
};
|
||||
} else {
|
||||
const value_start = parser.index;
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Spread',
|
||||
expression,
|
||||
};
|
||||
} else {
|
||||
const value_start = parser.index;
|
||||
|
||||
const name = parser.read_identifier();
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
const name = parser.read_identifier();
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
check_unique(name);
|
||||
check_unique(name);
|
||||
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value: [{
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'AttributeShorthand',
|
||||
expression: {
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'Identifier',
|
||||
name
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value: [
|
||||
{
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'AttributeShorthand',
|
||||
expression: {
|
||||
start: value_start,
|
||||
end: value_start + name.length,
|
||||
type: 'Identifier',
|
||||
name,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const name = parser.read_until(/[\s=\/>"']/);
|
||||
if (!name) return null;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const name = parser.read_until(/[\s=\/>"']/);
|
||||
if (!name) return null;
|
||||
|
||||
let end = parser.index;
|
||||
let end = parser.index;
|
||||
|
||||
parser.allow_whitespace();
|
||||
parser.allow_whitespace();
|
||||
|
||||
const colon_index = name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
||||
const colon_index = name.indexOf(':');
|
||||
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
|
||||
|
||||
let value: any[] | true = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
} else if (parser.match_regex(/["']/)) {
|
||||
parser.error({
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected ='
|
||||
}, parser.index);
|
||||
}
|
||||
let value: any[] | true = true;
|
||||
if (parser.eat('=')) {
|
||||
parser.allow_whitespace();
|
||||
value = read_attribute_value(parser);
|
||||
end = parser.index;
|
||||
} else if (parser.match_regex(/["']/)) {
|
||||
parser.error(
|
||||
{
|
||||
code: 'unexpected-token',
|
||||
message: 'Expected =',
|
||||
},
|
||||
parser.index
|
||||
);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
||||
if (type) {
|
||||
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
|
||||
|
||||
if (type === 'Binding' && directive_name !== 'this') {
|
||||
check_unique(directive_name);
|
||||
} else if (type !== 'EventHandler' && type !== 'Action') {
|
||||
check_unique(name);
|
||||
}
|
||||
if (type === 'Binding' && directive_name !== 'this') {
|
||||
check_unique(directive_name);
|
||||
} else if (type !== 'EventHandler' && type !== 'Action') {
|
||||
check_unique(name);
|
||||
}
|
||||
|
||||
if (type === 'Ref') {
|
||||
parser.error({
|
||||
code: 'invalid-ref-directive',
|
||||
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
|
||||
}, start);
|
||||
}
|
||||
if (type === 'Ref') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-ref-directive',
|
||||
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
|
||||
},
|
||||
start
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'Class' && directive_name === '') {
|
||||
parser.error({
|
||||
code: 'invalid-class-directive',
|
||||
message: 'Class binding name cannot be empty'
|
||||
}, start + colon_index + 1);
|
||||
}
|
||||
if (type === 'Class' && directive_name === '') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-class-directive',
|
||||
message: 'Class binding name cannot be empty',
|
||||
},
|
||||
start + colon_index + 1
|
||||
);
|
||||
}
|
||||
|
||||
if (value[0]) {
|
||||
if ((value as any[]).length > 1 || value[0].type === 'Text') {
|
||||
parser.error({
|
||||
code: 'invalid-directive-value',
|
||||
message: 'Directive value must be a JavaScript expression enclosed in curly braces'
|
||||
}, value[0].start);
|
||||
}
|
||||
}
|
||||
if (value[0]) {
|
||||
if ((value as any[]).length > 1 || value[0].type === 'Text') {
|
||||
parser.error(
|
||||
{
|
||||
code: 'invalid-directive-value',
|
||||
message: 'Directive value must be a JavaScript expression enclosed in curly braces',
|
||||
},
|
||||
value[0].start
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const directive: Directive = {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
modifiers,
|
||||
expression: (value[0] && value[0].expression) || null
|
||||
};
|
||||
const directive: Directive = {
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
name: directive_name,
|
||||
modifiers,
|
||||
expression: (value[0] && value[0].expression) || null,
|
||||
};
|
||||
|
||||
if (type === 'Transition') {
|
||||
const direction = name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
if (type === 'Transition') {
|
||||
const direction = name.slice(0, colon_index);
|
||||
directive.intro = direction === 'in' || direction === 'transition';
|
||||
directive.outro = direction === 'out' || direction === 'transition';
|
||||
}
|
||||
|
||||
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
|
||||
directive.expression = {
|
||||
start: directive.start + colon_index + 1,
|
||||
end: directive.end,
|
||||
type: 'Identifier',
|
||||
name: directive.name
|
||||
} as any;
|
||||
}
|
||||
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
|
||||
directive.expression = {
|
||||
start: directive.start + colon_index + 1,
|
||||
end: directive.end,
|
||||
type: 'Identifier',
|
||||
name: directive.name,
|
||||
} as any;
|
||||
}
|
||||
|
||||
return directive;
|
||||
}
|
||||
return directive;
|
||||
}
|
||||
|
||||
check_unique(name);
|
||||
check_unique(name);
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value
|
||||
};
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
type: 'Attribute',
|
||||
name,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function get_directive_type(name: string): DirectiveType {
|
||||
if (name === 'use') return 'Action';
|
||||
if (name === 'animate') return 'Animation';
|
||||
if (name === 'bind') return 'Binding';
|
||||
if (name === 'class') return 'Class';
|
||||
if (name === 'on') return 'EventHandler';
|
||||
if (name === 'let') return 'Let';
|
||||
if (name === 'ref') return 'Ref';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
|
||||
if (name === 'use') return 'Action';
|
||||
if (name === 'animate') return 'Animation';
|
||||
if (name === 'bind') return 'Binding';
|
||||
if (name === 'class') return 'Class';
|
||||
if (name === 'on') return 'EventHandler';
|
||||
if (name === 'let') return 'Let';
|
||||
if (name === 'ref') return 'Ref';
|
||||
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
|
||||
}
|
||||
|
||||
function read_attribute_value(parser: Parser) {
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
|
||||
|
||||
const regex = (
|
||||
quote_mark === "'" ? /'/ :
|
||||
quote_mark === '"' ? /"/ :
|
||||
/(\/>|[\s"'=<>`])/
|
||||
);
|
||||
const regex = 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;
|
||||
return value;
|
||||
if (quote_mark) parser.index += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
|
||||
let current_chunk: Text = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null
|
||||
};
|
||||
let current_chunk: Text = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null,
|
||||
};
|
||||
|
||||
function flush() {
|
||||
if (current_chunk.raw) {
|
||||
current_chunk.data = decode_character_references(current_chunk.raw);
|
||||
current_chunk.end = parser.index;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
function flush() {
|
||||
if (current_chunk.raw) {
|
||||
current_chunk.data = decode_character_references(current_chunk.raw);
|
||||
current_chunk.end = parser.index;
|
||||
chunks.push(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
const chunks: TemplateNode[] = [];
|
||||
const chunks: TemplateNode[] = [];
|
||||
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
while (parser.index < parser.template.length) {
|
||||
const index = parser.index;
|
||||
|
||||
if (done()) {
|
||||
flush();
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
flush();
|
||||
if (done()) {
|
||||
flush();
|
||||
return chunks;
|
||||
} else if (parser.eat('{')) {
|
||||
flush();
|
||||
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
parser.allow_whitespace();
|
||||
const expression = read_expression(parser);
|
||||
parser.allow_whitespace();
|
||||
parser.eat('}', true);
|
||||
|
||||
chunks.push({
|
||||
start: index,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression
|
||||
});
|
||||
chunks.push({
|
||||
start: index,
|
||||
end: parser.index,
|
||||
type: 'MustacheTag',
|
||||
expression,
|
||||
});
|
||||
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null
|
||||
};
|
||||
} else {
|
||||
current_chunk.raw += parser.template[parser.index++];
|
||||
}
|
||||
}
|
||||
current_chunk = {
|
||||
start: parser.index,
|
||||
end: null,
|
||||
type: 'Text',
|
||||
raw: '',
|
||||
data: null,
|
||||
};
|
||||
} else {
|
||||
current_chunk.raw += parser.template[parser.index++];
|
||||
}
|
||||
}
|
||||
|
||||
parser.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input'
|
||||
});
|
||||
parser.error({
|
||||
code: 'unexpected-eof',
|
||||
message: 'Unexpected end of input',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,25 +4,21 @@ import { decode_character_references } from '../utils/html.js';
|
|||
import { Parser } from '../index.js';
|
||||
|
||||
export default function text(parser: Parser) {
|
||||
const start = parser.index;
|
||||
const start = parser.index;
|
||||
|
||||
let data = '';
|
||||
let data = '';
|
||||
|
||||
while (
|
||||
parser.index < parser.template.length &&
|
||||
!parser.match('<') &&
|
||||
!parser.match('{')
|
||||
) {
|
||||
data += parser.template[parser.index++];
|
||||
}
|
||||
while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
|
||||
data += parser.template[parser.index++];
|
||||
}
|
||||
|
||||
const node = {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Text',
|
||||
raw: data,
|
||||
data: decode_character_references(data)
|
||||
};
|
||||
const node = {
|
||||
start,
|
||||
end: parser.index,
|
||||
type: 'Text',
|
||||
raw: 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);
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return (
|
||||
(open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) ||
|
||||
(open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE)
|
||||
);
|
||||
return (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE);
|
||||
}
|
||||
|
||||
export function get_bracket_close(open) {
|
||||
if (open === SQUARE_BRACKET_OPEN) {
|
||||
return SQUARE_BRACKET_CLOSE;
|
||||
}
|
||||
if (open === CURLY_BRACKET_OPEN) {
|
||||
return CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
if (open === SQUARE_BRACKET_OPEN) {
|
||||
return SQUARE_BRACKET_CLOSE;
|
||||
}
|
||||
if (open === CURLY_BRACKET_OPEN) {
|
||||
return CURLY_BRACKET_CLOSE;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,64 +3,61 @@
|
|||
import entities from './entities.js';
|
||||
|
||||
const windows_1252 = [
|
||||
8364,
|
||||
129,
|
||||
8218,
|
||||
402,
|
||||
8222,
|
||||
8230,
|
||||
8224,
|
||||
8225,
|
||||
710,
|
||||
8240,
|
||||
352,
|
||||
8249,
|
||||
338,
|
||||
141,
|
||||
381,
|
||||
143,
|
||||
144,
|
||||
8216,
|
||||
8217,
|
||||
8220,
|
||||
8221,
|
||||
8226,
|
||||
8211,
|
||||
8212,
|
||||
732,
|
||||
8482,
|
||||
353,
|
||||
8250,
|
||||
339,
|
||||
157,
|
||||
382,
|
||||
376
|
||||
8364,
|
||||
129,
|
||||
8218,
|
||||
402,
|
||||
8222,
|
||||
8230,
|
||||
8224,
|
||||
8225,
|
||||
710,
|
||||
8240,
|
||||
352,
|
||||
8249,
|
||||
338,
|
||||
141,
|
||||
381,
|
||||
143,
|
||||
144,
|
||||
8216,
|
||||
8217,
|
||||
8220,
|
||||
8221,
|
||||
8226,
|
||||
8211,
|
||||
8212,
|
||||
732,
|
||||
8482,
|
||||
353,
|
||||
8250,
|
||||
339,
|
||||
157,
|
||||
382,
|
||||
376,
|
||||
];
|
||||
|
||||
const entity_pattern = new RegExp(
|
||||
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
|
||||
'g'
|
||||
);
|
||||
const entity_pattern = new RegExp(`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, 'g');
|
||||
|
||||
export function decode_character_references(html: string) {
|
||||
return html.replace(entity_pattern, (match, entity) => {
|
||||
let code;
|
||||
return html.replace(entity_pattern, (match, entity) => {
|
||||
let code;
|
||||
|
||||
// Handle named entities
|
||||
if (entity[0] !== '#') {
|
||||
code = entities[entity];
|
||||
} else if (entity[1] === 'x') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
// Handle named entities
|
||||
if (entity[0] !== '#') {
|
||||
code = entities[entity];
|
||||
} else if (entity[1] === 'x') {
|
||||
code = parseInt(entity.substring(2), 16);
|
||||
} else {
|
||||
code = parseInt(entity.substring(1), 10);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
if (!code) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return String.fromCodePoint(validate_code(code));
|
||||
});
|
||||
return String.fromCodePoint(validate_code(code));
|
||||
});
|
||||
}
|
||||
|
||||
const NUL = 0;
|
||||
|
@ -71,83 +68,76 @@ const NUL = 0;
|
|||
//
|
||||
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
|
||||
function validate_code(code: number) {
|
||||
// line feed becomes generic whitespace
|
||||
if (code === 10) {
|
||||
return 32;
|
||||
}
|
||||
// line feed becomes generic whitespace
|
||||
if (code === 10) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||
if (code < 128) {
|
||||
return code;
|
||||
}
|
||||
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
|
||||
if (code < 128) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (code <= 159) {
|
||||
return windows_1252[code - 128];
|
||||
}
|
||||
// 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
|
||||
if (code <= 159) {
|
||||
return windows_1252[code - 128];
|
||||
}
|
||||
|
||||
// basic multilingual plane
|
||||
if (code < 55296) {
|
||||
return code;
|
||||
}
|
||||
// basic multilingual plane
|
||||
if (code < 55296) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// UTF-16 surrogate halves
|
||||
if (code <= 57343) {
|
||||
return NUL;
|
||||
}
|
||||
// UTF-16 surrogate halves
|
||||
if (code <= 57343) {
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// rest of the basic multilingual plane
|
||||
if (code <= 65535) {
|
||||
return code;
|
||||
}
|
||||
// rest of the basic multilingual plane
|
||||
if (code <= 65535) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||
if (code >= 65536 && code <= 131071) {
|
||||
return code;
|
||||
}
|
||||
// supplementary multilingual plane 0x10000 - 0x1ffff
|
||||
if (code >= 65536 && code <= 131071) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||
if (code >= 131072 && code <= 196607) {
|
||||
return code;
|
||||
}
|
||||
// supplementary ideographic plane 0x20000 - 0x2ffff
|
||||
if (code >= 131072 && code <= 196607) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return NUL;
|
||||
return NUL;
|
||||
}
|
||||
|
||||
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
|
||||
const disallowed_contents = new Map([
|
||||
['li', new Set(['li'])],
|
||||
['dt', 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(
|
||||
' '
|
||||
)
|
||||
)
|
||||
],
|
||||
['rt', new Set(['rt', 'rp'])],
|
||||
['rp', new Set(['rt', 'rp'])],
|
||||
['optgroup', new Set(['optgroup'])],
|
||||
['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'])]
|
||||
['li', new Set(['li'])],
|
||||
['dt', 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(' '))],
|
||||
['rt', new Set(['rt', 'rp'])],
|
||||
['rp', new Set(['rt', 'rp'])],
|
||||
['optgroup', new Set(['optgroup'])],
|
||||
['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
|
||||
// close it, like `<li>one<li>two`?
|
||||
export function closing_tag_omitted(current: string, next?: string) {
|
||||
if (disallowed_contents.has(current)) {
|
||||
if (!next || disallowed_contents.get(current).has(next)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (disallowed_contents.has(current)) {
|
||||
if (!next || disallowed_contents.get(current).has(next)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import { TemplateNode } from '../../interfaces.js';
|
||||
|
||||
export function to_string(node: TemplateNode) {
|
||||
switch (node.type) {
|
||||
case 'IfBlock':
|
||||
return '{#if} block';
|
||||
case 'ThenBlock':
|
||||
return '{:then} block';
|
||||
case 'ElseBlock':
|
||||
return '{:else} block';
|
||||
case 'PendingBlock':
|
||||
case 'AwaitBlock':
|
||||
return '{#await} block';
|
||||
case 'CatchBlock':
|
||||
return '{:catch} block';
|
||||
case 'EachBlock':
|
||||
return '{#each} block';
|
||||
case 'RawMustacheTag':
|
||||
return '{@html} block';
|
||||
case 'DebugTag':
|
||||
return '{@debug} block';
|
||||
case 'Element':
|
||||
case 'InlineComponent':
|
||||
case 'Slot':
|
||||
case 'Title':
|
||||
return `<${node.name}> tag`;
|
||||
default:
|
||||
return node.type;
|
||||
}
|
||||
switch (node.type) {
|
||||
case 'IfBlock':
|
||||
return '{#if} block';
|
||||
case 'ThenBlock':
|
||||
return '{:then} block';
|
||||
case 'ElseBlock':
|
||||
return '{:else} block';
|
||||
case 'PendingBlock':
|
||||
case 'AwaitBlock':
|
||||
return '{#await} block';
|
||||
case 'CatchBlock':
|
||||
return '{:catch} block';
|
||||
case 'EachBlock':
|
||||
return '{#each} block';
|
||||
case 'RawMustacheTag':
|
||||
return '{@html} block';
|
||||
case 'DebugTag':
|
||||
return '{@debug} block';
|
||||
case 'Element':
|
||||
case 'InlineComponent':
|
||||
case 'Slot':
|
||||
case 'Title':
|
||||
return `<${node.name}> tag`;
|
||||
default:
|
||||
return node.type;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,39 +4,42 @@ import { locate } from 'locate-character';
|
|||
import get_code_frame from './get_code_frame.js';
|
||||
|
||||
export class CompileError extends Error {
|
||||
code: string;
|
||||
start: { line: number; column: number };
|
||||
end: { line: number; column: number };
|
||||
pos: number;
|
||||
filename: string;
|
||||
frame: string;
|
||||
code: string;
|
||||
start: { line: number; column: number };
|
||||
end: { line: number; column: number };
|
||||
pos: number;
|
||||
filename: string;
|
||||
frame: string;
|
||||
|
||||
toString() {
|
||||
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
|
||||
}
|
||||
toString() {
|
||||
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function error(message: string, props: {
|
||||
name: string;
|
||||
code: string;
|
||||
source: string;
|
||||
filename: string;
|
||||
start: number;
|
||||
end?: number;
|
||||
}): never {
|
||||
const error = new CompileError(message);
|
||||
error.name = props.name;
|
||||
export default function error(
|
||||
message: string,
|
||||
props: {
|
||||
name: string;
|
||||
code: string;
|
||||
source: string;
|
||||
filename: string;
|
||||
start: number;
|
||||
end?: number;
|
||||
}
|
||||
): never {
|
||||
const error = new CompileError(message);
|
||||
error.name = props.name;
|
||||
|
||||
const start = locate(props.source, props.start, { offsetLine: 1 });
|
||||
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
|
||||
const start = locate(props.source, props.start, { offsetLine: 1 });
|
||||
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
|
||||
|
||||
error.code = props.code;
|
||||
error.start = start;
|
||||
error.end = end;
|
||||
error.pos = props.start;
|
||||
error.filename = props.filename;
|
||||
error.code = props.code;
|
||||
error.start = start;
|
||||
error.end = end;
|
||||
error.pos = props.start;
|
||||
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
|
||||
|
||||
export default function full_char_code_at(str: string, i: number): number {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code <= 0xd7ff || code >= 0xe000) return code;
|
||||
const code = str.charCodeAt(i);
|
||||
if (code <= 0xd7ff || code >= 0xe000) return code;
|
||||
|
||||
const next = str.charCodeAt(i + 1);
|
||||
return (code << 10) + next - 0x35fdc00;
|
||||
const next = str.charCodeAt(i + 1);
|
||||
return (code << 10) + next - 0x35fdc00;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// @ts-nocheck
|
||||
|
||||
export default function fuzzymatch(name: string, names: string[]) {
|
||||
const set = new FuzzySet(names);
|
||||
const matches = set.get(name);
|
||||
const set = new FuzzySet(names);
|
||||
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
|
||||
|
@ -15,225 +15,215 @@ const GRAM_SIZE_UPPER = 3;
|
|||
|
||||
// return an edit distance from 0 to 1
|
||||
function _distance(str1: string, str2: string) {
|
||||
if (str1 === null && str2 === null) {
|
||||
throw 'Trying to compare two null values';
|
||||
}
|
||||
if (str1 === null || str2 === null) return 0;
|
||||
str1 = String(str1);
|
||||
str2 = String(str2);
|
||||
if (str1 === null && str2 === null) {
|
||||
throw 'Trying to compare two null values';
|
||||
}
|
||||
if (str1 === null || str2 === null) return 0;
|
||||
str1 = String(str1);
|
||||
str2 = String(str2);
|
||||
|
||||
const distance = levenshtein(str1, str2);
|
||||
if (str1.length > str2.length) {
|
||||
return 1 - distance / str1.length;
|
||||
} else {
|
||||
return 1 - distance / str2.length;
|
||||
}
|
||||
const distance = levenshtein(str1, str2);
|
||||
if (str1.length > str2.length) {
|
||||
return 1 - distance / str1.length;
|
||||
} else {
|
||||
return 1 - distance / str2.length;
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
function levenshtein(str1: string, str2: string) {
|
||||
const current: number[] = [];
|
||||
let prev;
|
||||
let value;
|
||||
const current: number[] = [];
|
||||
let prev;
|
||||
let value;
|
||||
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
if (i && j) {
|
||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||
value = prev;
|
||||
} else {
|
||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||
}
|
||||
} else {
|
||||
value = i + j;
|
||||
}
|
||||
for (let i = 0; i <= str2.length; i++) {
|
||||
for (let j = 0; j <= str1.length; j++) {
|
||||
if (i && j) {
|
||||
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
|
||||
value = prev;
|
||||
} else {
|
||||
value = Math.min(current[j], current[j - 1], prev) + 1;
|
||||
}
|
||||
} else {
|
||||
value = i + j;
|
||||
}
|
||||
|
||||
prev = current[j];
|
||||
current[j] = value;
|
||||
}
|
||||
}
|
||||
prev = current[j];
|
||||
current[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return current.pop();
|
||||
return current.pop();
|
||||
}
|
||||
|
||||
const non_word_regex = /[^\w, ]+/;
|
||||
|
||||
function iterate_grams(value: string, gram_size = 2) {
|
||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||
const len_diff = gram_size - simplified.length;
|
||||
const results = [];
|
||||
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
|
||||
const len_diff = gram_size - simplified.length;
|
||||
const results = [];
|
||||
|
||||
if (len_diff > 0) {
|
||||
for (let i = 0; i < len_diff; ++i) {
|
||||
value += '-';
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||
results.push(simplified.slice(i, i + gram_size));
|
||||
}
|
||||
return results;
|
||||
if (len_diff > 0) {
|
||||
for (let i = 0; i < len_diff; ++i) {
|
||||
value += '-';
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
|
||||
results.push(simplified.slice(i, i + gram_size));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function gram_counter(value: string, gram_size = 2) {
|
||||
// return an object where key=gram, value=number of occurrences
|
||||
const result = {};
|
||||
const grams = iterate_grams(value, gram_size);
|
||||
let i = 0;
|
||||
// return an object where key=gram, value=number of occurrences
|
||||
const result = {};
|
||||
const grams = iterate_grams(value, gram_size);
|
||||
let i = 0;
|
||||
|
||||
for (i; i < grams.length; ++i) {
|
||||
if (grams[i] in result) {
|
||||
result[grams[i]] += 1;
|
||||
} else {
|
||||
result[grams[i]] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
for (i; i < grams.length; ++i) {
|
||||
if (grams[i] in result) {
|
||||
result[grams[i]] += 1;
|
||||
} else {
|
||||
result[grams[i]] = 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sort_descending(a, b) {
|
||||
return b[0] - a[0];
|
||||
return b[0] - a[0];
|
||||
}
|
||||
|
||||
class FuzzySet {
|
||||
exact_set = {};
|
||||
match_dict = {};
|
||||
items = {};
|
||||
exact_set = {};
|
||||
match_dict = {};
|
||||
items = {};
|
||||
|
||||
constructor(arr: string[]) {
|
||||
// initialization
|
||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this.items[i] = [];
|
||||
}
|
||||
constructor(arr: string[]) {
|
||||
// initialization
|
||||
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this.items[i] = [];
|
||||
}
|
||||
|
||||
// add all the items to the set
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
this.add(arr[i]);
|
||||
}
|
||||
}
|
||||
// add all the items to the set
|
||||
for (let i = 0; i < arr.length; ++i) {
|
||||
this.add(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
add(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
if (normalized_value in this.exact_set) {
|
||||
return false;
|
||||
}
|
||||
add(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
if (normalized_value in this.exact_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i = GRAM_SIZE_LOWER;
|
||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this._add(value, i);
|
||||
}
|
||||
}
|
||||
let i = GRAM_SIZE_LOWER;
|
||||
for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
|
||||
this._add(value, i);
|
||||
}
|
||||
}
|
||||
|
||||
_add(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const items = this.items[gram_size] || [];
|
||||
const index = items.length;
|
||||
_add(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const items = this.items[gram_size] || [];
|
||||
const index = items.length;
|
||||
|
||||
items.push(0);
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
items.push(0);
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
this.match_dict[gram].push([index, gram_count]);
|
||||
} else {
|
||||
this.match_dict[gram] = [[index, gram_count]];
|
||||
}
|
||||
}
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
items[index] = [vector_normal, normalized_value];
|
||||
this.items[gram_size] = items;
|
||||
this.exact_set[normalized_value] = value;
|
||||
}
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
this.match_dict[gram].push([index, gram_count]);
|
||||
} else {
|
||||
this.match_dict[gram] = [[index, gram_count]];
|
||||
}
|
||||
}
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
items[index] = [vector_normal, normalized_value];
|
||||
this.items[gram_size] = items;
|
||||
this.exact_set[normalized_value] = value;
|
||||
}
|
||||
|
||||
get(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const result = this.exact_set[normalized_value];
|
||||
get(value: string) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const result = this.exact_set[normalized_value];
|
||||
|
||||
if (result) {
|
||||
return [[1, result]];
|
||||
}
|
||||
if (result) {
|
||||
return [[1, result]];
|
||||
}
|
||||
|
||||
let results = [];
|
||||
// start with high gram size and if there are no results, go to lower gram sizes
|
||||
for (
|
||||
let gram_size = GRAM_SIZE_UPPER;
|
||||
gram_size >= GRAM_SIZE_LOWER;
|
||||
--gram_size
|
||||
) {
|
||||
results = this.__get(value, gram_size);
|
||||
if (results) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let results = [];
|
||||
// start with high gram size and if there are no results, go to lower gram sizes
|
||||
for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
|
||||
results = this.__get(value, gram_size);
|
||||
if (results) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
__get(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const matches = {};
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
const items = this.items[gram_size];
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
let i;
|
||||
let index;
|
||||
let other_gram_count;
|
||||
__get(value: string, gram_size: number) {
|
||||
const normalized_value = value.toLowerCase();
|
||||
const matches = {};
|
||||
const gram_counts = gram_counter(normalized_value, gram_size);
|
||||
const items = this.items[gram_size];
|
||||
let sum_of_square_gram_counts = 0;
|
||||
let gram;
|
||||
let gram_count;
|
||||
let i;
|
||||
let index;
|
||||
let other_gram_count;
|
||||
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||
index = this.match_dict[gram][i][0];
|
||||
other_gram_count = this.match_dict[gram][i][1];
|
||||
if (index in matches) {
|
||||
matches[index] += gram_count * other_gram_count;
|
||||
} else {
|
||||
matches[index] = gram_count * other_gram_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (gram in gram_counts) {
|
||||
gram_count = gram_counts[gram];
|
||||
sum_of_square_gram_counts += Math.pow(gram_count, 2);
|
||||
if (gram in this.match_dict) {
|
||||
for (i = 0; i < this.match_dict[gram].length; ++i) {
|
||||
index = this.match_dict[gram][i][0];
|
||||
other_gram_count = this.match_dict[gram][i][1];
|
||||
if (index in matches) {
|
||||
matches[index] += gram_count * other_gram_count;
|
||||
} else {
|
||||
matches[index] = gram_count * other_gram_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
let results = [];
|
||||
let match_score;
|
||||
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
|
||||
let results = [];
|
||||
let match_score;
|
||||
|
||||
// build a results list of [score, str]
|
||||
for (const match_index in matches) {
|
||||
match_score = matches[match_index];
|
||||
results.push([
|
||||
match_score / (vector_normal * items[match_index][0]),
|
||||
items[match_index][1]
|
||||
]);
|
||||
}
|
||||
// build a results list of [score, str]
|
||||
for (const match_index in matches) {
|
||||
match_score = matches[match_index];
|
||||
results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
|
||||
}
|
||||
|
||||
results.sort(sort_descending);
|
||||
results.sort(sort_descending);
|
||||
|
||||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
new_results.push([
|
||||
_distance(results[i][1], normalized_value),
|
||||
results[i][1]
|
||||
]);
|
||||
}
|
||||
results = new_results;
|
||||
results.sort(sort_descending);
|
||||
let new_results = [];
|
||||
const end_index = Math.min(50, results.length);
|
||||
// truncate somewhat arbitrarily to 50
|
||||
for (let i = 0; i < end_index; ++i) {
|
||||
new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
|
||||
}
|
||||
results = new_results;
|
||||
results.sort(sort_descending);
|
||||
|
||||
new_results = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] == results[0][0]) {
|
||||
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
|
||||
}
|
||||
}
|
||||
new_results = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
if (results[i][0] == results[0][0]) {
|
||||
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) {
|
||||
return str.replace(/^\t+/, match => match.split('\t').join(' '));
|
||||
return str.replace(/^\t+/, (match) => match.split('\t').join(' '));
|
||||
}
|
||||
|
||||
export default function get_code_frame(
|
||||
source: string,
|
||||
line: number,
|
||||
column: number
|
||||
) {
|
||||
const lines = source.split('\n');
|
||||
export default function get_code_frame(source: string, line: number, column: number) {
|
||||
const lines = source.split('\n');
|
||||
|
||||
const frame_start = Math.max(0, line - 2);
|
||||
const frame_end = Math.min(line + 3, lines.length);
|
||||
const frame_start = Math.max(0, line - 2);
|
||||
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
|
||||
.slice(frame_start, frame_end)
|
||||
.map((str, i) => {
|
||||
const isErrorLine = frame_start + i === line;
|
||||
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
|
||||
return lines
|
||||
.slice(frame_start, frame_end)
|
||||
.map((str, i) => {
|
||||
const isErrorLine = frame_start + i === line;
|
||||
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
|
||||
|
||||
if (isErrorLine) {
|
||||
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
|
||||
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
|
||||
}
|
||||
if (isErrorLine) {
|
||||
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)}`;
|
||||
})
|
||||
.join('\n');
|
||||
return `${line_num}: ${tabs_to_spaces(str)}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
|
||||
prev.next = next;
|
||||
if (next) next.prev = prev;
|
||||
prev.next = next;
|
||||
if (next) next.prev = prev;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
export default function list(items: string[], conjunction = 'or') {
|
||||
if (items.length === 1) return items[0];
|
||||
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[
|
||||
items.length - 1
|
||||
]}`;
|
||||
if (items.length === 1) return items[0];
|
||||
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
|
||||
}
|
||||
|
|
|
@ -2,138 +2,138 @@ import { isIdentifierStart, isIdentifierChar } from 'acorn';
|
|||
import full_char_code_at from './full_char_code_at.js';
|
||||
|
||||
export const globals = new Set([
|
||||
'alert',
|
||||
'Array',
|
||||
'Boolean',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'confirm',
|
||||
'console',
|
||||
'Date',
|
||||
'decodeURI',
|
||||
'decodeURIComponent',
|
||||
'document',
|
||||
'Element',
|
||||
'encodeURI',
|
||||
'encodeURIComponent',
|
||||
'Error',
|
||||
'EvalError',
|
||||
'Event',
|
||||
'EventSource',
|
||||
'fetch',
|
||||
'global',
|
||||
'globalThis',
|
||||
'history',
|
||||
'Infinity',
|
||||
'InternalError',
|
||||
'Intl',
|
||||
'isFinite',
|
||||
'isNaN',
|
||||
'JSON',
|
||||
'localStorage',
|
||||
'location',
|
||||
'Map',
|
||||
'Math',
|
||||
'NaN',
|
||||
'navigator',
|
||||
'Number',
|
||||
'Node',
|
||||
'Object',
|
||||
'parseFloat',
|
||||
'parseInt',
|
||||
'process',
|
||||
'Promise',
|
||||
'prompt',
|
||||
'RangeError',
|
||||
'ReferenceError',
|
||||
'RegExp',
|
||||
'sessionStorage',
|
||||
'Set',
|
||||
'setInterval',
|
||||
'setTimeout',
|
||||
'String',
|
||||
'SyntaxError',
|
||||
'TypeError',
|
||||
'undefined',
|
||||
'URIError',
|
||||
'URL',
|
||||
'window'
|
||||
'alert',
|
||||
'Array',
|
||||
'Boolean',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'confirm',
|
||||
'console',
|
||||
'Date',
|
||||
'decodeURI',
|
||||
'decodeURIComponent',
|
||||
'document',
|
||||
'Element',
|
||||
'encodeURI',
|
||||
'encodeURIComponent',
|
||||
'Error',
|
||||
'EvalError',
|
||||
'Event',
|
||||
'EventSource',
|
||||
'fetch',
|
||||
'global',
|
||||
'globalThis',
|
||||
'history',
|
||||
'Infinity',
|
||||
'InternalError',
|
||||
'Intl',
|
||||
'isFinite',
|
||||
'isNaN',
|
||||
'JSON',
|
||||
'localStorage',
|
||||
'location',
|
||||
'Map',
|
||||
'Math',
|
||||
'NaN',
|
||||
'navigator',
|
||||
'Number',
|
||||
'Node',
|
||||
'Object',
|
||||
'parseFloat',
|
||||
'parseInt',
|
||||
'process',
|
||||
'Promise',
|
||||
'prompt',
|
||||
'RangeError',
|
||||
'ReferenceError',
|
||||
'RegExp',
|
||||
'sessionStorage',
|
||||
'Set',
|
||||
'setInterval',
|
||||
'setTimeout',
|
||||
'String',
|
||||
'SyntaxError',
|
||||
'TypeError',
|
||||
'undefined',
|
||||
'URIError',
|
||||
'URL',
|
||||
'window',
|
||||
]);
|
||||
|
||||
export const reserved = new Set([
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield'
|
||||
'arguments',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'interface',
|
||||
'let',
|
||||
'new',
|
||||
'null',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
]);
|
||||
|
||||
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) {
|
||||
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 {
|
||||
let i = 0;
|
||||
let i = 0;
|
||||
|
||||
while (i < str.length) {
|
||||
const code = full_char_code_at(str, i);
|
||||
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
|
||||
while (i < str.length) {
|
||||
const code = full_char_code_at(str, i);
|
||||
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) {
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
||||
.replace(/^_/, '')
|
||||
.replace(/_$/, '')
|
||||
.replace(/^[0-9]/, '_$&');
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9_]+/g, '_')
|
||||
.replace(/^_/, '')
|
||||
.replace(/_$/, '')
|
||||
.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 xmlns = 'http://www.w3.org/2000/xmlns';
|
||||
|
||||
export const valid_namespaces = [
|
||||
'foreign',
|
||||
'html',
|
||||
'mathml',
|
||||
'svg',
|
||||
'xlink',
|
||||
'xml',
|
||||
'xmlns',
|
||||
foreign,
|
||||
html,
|
||||
mathml,
|
||||
svg,
|
||||
xlink,
|
||||
xml,
|
||||
xmlns
|
||||
];
|
||||
export const valid_namespaces = ['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 };
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
// @ts-nocheck
|
||||
|
||||
export function nodes_match(a, b) {
|
||||
if (!!a !== !!b) return false;
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
if (!!a !== !!b) return false;
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((child, i) => nodes_match(child, b[i]));
|
||||
}
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((child, i) => nodes_match(child, b[i]));
|
||||
}
|
||||
|
||||
const a_keys = Object.keys(a).sort();
|
||||
const b_keys = Object.keys(b).sort();
|
||||
const a_keys = Object.keys(a).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;
|
||||
while (i--) {
|
||||
const key = a_keys[i];
|
||||
if (b_keys[i] !== key) return false;
|
||||
let i = a_keys.length;
|
||||
while (i--) {
|
||||
const key = a_keys[i];
|
||||
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])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!nodes_match(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
return a === b;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { whitespace } from './patterns.js';
|
||||
|
||||
export function trim_start(str: string) {
|
||||
let i = 0;
|
||||
while (whitespace.test(str[i])) i += 1;
|
||||
let i = 0;
|
||||
while (whitespace.test(str[i])) i += 1;
|
||||
|
||||
return str.slice(i);
|
||||
return str.slice(i);
|
||||
}
|
||||
|
||||
export function trim_end(str: string) {
|
||||
let i = str.length;
|
||||
while (whitespace.test(str[i - 1])) i -= 1;
|
||||
let i = str.length;
|
||||
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 { 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 { relative as pathRelative } from 'path';
|
||||
import { defaultLogDestination, info, error, parseError } from './logger.js';
|
||||
|
||||
const { readFile } = fsPromises;
|
||||
import { defaultLogDestination, error, parseError } from './logger.js';
|
||||
import { createRuntime } from './runtime.js';
|
||||
|
||||
const hostname = '127.0.0.1';
|
||||
const port = 3000;
|
||||
|
@ -20,95 +19,45 @@ const logging: LogOptions = {
|
|||
};
|
||||
|
||||
export default async function (astroConfig: AstroConfig) {
|
||||
const { projectRoot, hmxRoot } = astroConfig;
|
||||
const { projectRoot } = astroConfig;
|
||||
|
||||
const internalPath = new URL('./frontend/', import.meta.url);
|
||||
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 runtime = await createRuntime(astroConfig, logging);
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const fullurl = new URL(req.url || '/', 'https://example.org/');
|
||||
const reqPath = decodeURI(fullurl.pathname);
|
||||
const selectedPage = reqPath.substr(1) || 'index';
|
||||
info(logging, 'access', reqPath);
|
||||
const result = await runtime.load(req.url);
|
||||
|
||||
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);
|
||||
switch (result.statusCode) {
|
||||
case 200: {
|
||||
if (result.contentType) {
|
||||
res.setHeader('Content-Type', result.contentType);
|
||||
}
|
||||
res.write(result.contents);
|
||||
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);
|
||||
res.statusCode = 404;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end('Not Found');
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mod = await runtime.importModule(selectedPageUrl);
|
||||
const html = await mod.exports.default();
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(html);
|
||||
} catch (err) {
|
||||
switch (err.code) {
|
||||
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;
|
||||
case 500: {
|
||||
switch (result.type) {
|
||||
case 'parse-error': {
|
||||
const err = result.error;
|
||||
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
||||
parseError(logging, err);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
error(logging, 'executing hmx', result.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import type { CompileError } from './compiler/utils/error.js';
|
||||
import { bold, blue, red, grey, underline } from 'kleur/colors';
|
||||
import { Writable } from 'stream';
|
||||
import { format as utilFormat } from 'util';
|
||||
|
||||
type ConsoleStream = Writable & {
|
||||
fd: 1 | 2
|
||||
fd: 1 | 2;
|
||||
};
|
||||
|
||||
export const defaultLogDestination = new Writable({
|
||||
objectMode: true,
|
||||
write(event: LogMessage, _, callback) {
|
||||
let dest: ConsoleStream = process.stderr;
|
||||
if(levels[event.level] < levels['error']) {
|
||||
if (levels[event.level] < levels['error']) {
|
||||
dest = process.stdout;
|
||||
}
|
||||
let type = event.type;
|
||||
if(event.level === 'info') {
|
||||
if (event.level === 'info') {
|
||||
type = bold(blue(type));
|
||||
} else if(event.level === 'error') {
|
||||
} else if (event.level === 'error') {
|
||||
type = bold(red(type));
|
||||
}
|
||||
|
||||
dest.write(`[${type}] `);
|
||||
dest.write(event.message);
|
||||
dest.write(utilFormat(...event.args));
|
||||
dest.write('\n');
|
||||
|
||||
callback();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
interface LogWritable<T> extends Writable {
|
||||
|
@ -37,18 +38,19 @@ export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
|
|||
|
||||
export interface LogOptions {
|
||||
dest: LogWritable<LogMessage>;
|
||||
level: LoggerLevel
|
||||
level: LoggerLevel;
|
||||
}
|
||||
|
||||
export const defaultLogOptions: LogOptions = {
|
||||
dest: defaultLogDestination,
|
||||
level: 'info'
|
||||
level: 'info',
|
||||
};
|
||||
|
||||
export interface LogMessage {
|
||||
type: string;
|
||||
level: LoggerLevel,
|
||||
level: LoggerLevel;
|
||||
message: string;
|
||||
args: Array<any>;
|
||||
}
|
||||
|
||||
const levels: Record<LoggerLevel, number> = {
|
||||
|
@ -59,19 +61,14 @@ const levels: Record<LoggerLevel, number> = {
|
|||
silent: 90,
|
||||
};
|
||||
|
||||
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...messages: Array<any>) {
|
||||
let event: LogMessage = {
|
||||
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
|
||||
const event: LogMessage = {
|
||||
type,
|
||||
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
|
||||
if (levels[opts.level] > levels[level]) {
|
||||
return; // do nothing
|
||||
|
@ -99,20 +96,24 @@ export function error(opts: LogOptions, type: string, ...messages: Array<any>) {
|
|||
export function parseError(opts: LogOptions, err: CompileError) {
|
||||
let frame = err.frame
|
||||
// Switch colons for pipes
|
||||
.replace(/^([0-9]+)(:)/mg, `${bold('$1')} │`)
|
||||
.replace(/^([0-9]+)(:)/gm, `${bold('$1')} │`)
|
||||
// Make the caret red.
|
||||
.replace(/(?<=^\s+)(\^)/mg, bold(red(' ^')))
|
||||
.replace(/(?<=^\s+)(\^)/gm, bold(red(' ^')))
|
||||
// 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}`)))}
|
||||
|
||||
${bold(red(`𝘅 ${err.message}`))}
|
||||
|
||||
${frame}
|
||||
`);
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// A default logger for when too lazy to pass LogOptions around.
|
||||
|
@ -120,5 +121,5 @@ export const logger = {
|
|||
debug: debug.bind(null, defaultLogOptions),
|
||||
info: info.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…
Reference in a new issue