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:
Matthew Phillips 2021-03-19 17:07:45 -04:00 committed by GitHub
parent 8ebc077cb0
commit 17c3c98f07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 4199 additions and 4026 deletions

26
.github/workflows/nodejs.yml vendored Normal file
View 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
View file

@ -536,7 +536,8 @@
"boolbase": { "boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
}, },
"boxen": { "boxen": {
"version": "4.2.0", "version": "4.2.0",
@ -684,67 +685,31 @@
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
}, },
"cheerio": { "cheerio": {
"version": "0.22.0", "version": "1.0.0-rc.5",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz",
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", "integrity": "sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==",
"dev": true,
"requires": { "requires": {
"css-select": "~1.2.0", "cheerio-select-tmp": "^0.1.0",
"dom-serializer": "~0.1.0", "dom-serializer": "~1.2.0",
"entities": "~1.1.1", "domhandler": "^4.0.0",
"htmlparser2": "^3.9.1", "entities": "~2.1.0",
"lodash.assignin": "^4.0.9", "htmlparser2": "^6.0.0",
"lodash.bind": "^4.1.4", "parse5": "^6.0.0",
"lodash.defaults": "^4.0.1", "parse5-htmlparser2-tree-adapter": "^6.0.0"
"lodash.filter": "^4.4.0", }
"lodash.flatten": "^4.2.0", },
"lodash.foreach": "^4.3.0", "cheerio-select-tmp": {
"lodash.map": "^4.4.0", "version": "0.1.1",
"lodash.merge": "^4.4.0", "resolved": "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz",
"lodash.pick": "^4.2.1", "integrity": "sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ==",
"lodash.reduce": "^4.4.0", "dev": true,
"lodash.reject": "^4.4.0", "requires": {
"lodash.some": "^4.4.0" "css-select": "^3.1.2",
}, "css-what": "^4.0.0",
"dependencies": { "domelementtype": "^2.1.0",
"domhandler": { "domhandler": "^4.0.0",
"version": "2.4.2", "domutils": "^2.4.4"
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"requires": {
"domelementtype": "1"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
}
} }
}, },
"chokidar": { "chokidar": {
@ -950,14 +915,16 @@
"dev": true "dev": true
}, },
"css-select": { "css-select": {
"version": "1.2.0", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==",
"dev": true,
"requires": { "requires": {
"boolbase": "~1.0.0", "boolbase": "^1.0.0",
"css-what": "2.1", "css-what": "^4.0.0",
"domutils": "1.5.1", "domhandler": "^4.0.0",
"nth-check": "~1.0.1" "domutils": "^2.4.3",
"nth-check": "^2.0.0"
} }
}, },
"css-tree": { "css-tree": {
@ -970,9 +937,10 @@
} }
}, },
"css-what": { "css-what": {
"version": "2.1.3", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==",
"dev": true
}, },
"cssesc": { "cssesc": {
"version": "3.0.0", "version": "3.0.0",
@ -1019,6 +987,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true "dev": true
}, },
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"default-browser-id": { "default-browser-id": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-2.0.0.tgz",
@ -1045,6 +1018,18 @@
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
"dev": true "dev": true
}, },
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
"dev": true
},
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"dev": true
},
"dir-glob": { "dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1064,18 +1049,21 @@
} }
}, },
"dom-serializer": { "dom-serializer": {
"version": "0.1.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
"dev": true,
"requires": { "requires": {
"domelementtype": "^1.3.0", "domelementtype": "^2.0.1",
"entities": "^1.1.1" "domhandler": "^4.0.0",
"entities": "^2.0.0"
} }
}, },
"domelementtype": { "domelementtype": {
"version": "1.3.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"dev": true
}, },
"domhandler": { "domhandler": {
"version": "4.0.0", "version": "4.0.0",
@ -1093,12 +1081,14 @@
} }
}, },
"domutils": { "domutils": {
"version": "1.5.1", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.0.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "integrity": "sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==",
"dev": true,
"requires": { "requires": {
"dom-serializer": "0", "dom-serializer": "^1.0.1",
"domelementtype": "1" "domelementtype": "^2.0.1",
"domhandler": "^4.0.0"
} }
}, },
"dot-prop": { "dot-prop": {
@ -1151,9 +1141,10 @@
} }
}, },
"entities": { "entities": {
"version": "1.1.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
"dev": true
}, },
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
@ -1806,7 +1797,8 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
}, },
"ini": { "ini": {
"version": "1.3.7", "version": "1.3.7",
@ -2073,71 +2065,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash.assignin": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
"integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI="
},
"lodash.bind": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
"integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU="
},
"lodash.camelcase": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
}, },
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
"integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
},
"lodash.map": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
},
"lodash.reject": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz",
"integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU="
},
"lodash.some": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0="
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -2292,6 +2224,12 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true "dev": true
}, },
"mri": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
"integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==",
"dev": true
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -2390,11 +2328,12 @@
"dev": true "dev": true
}, },
"nth-check": { "nth-check": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
"dev": true,
"requires": { "requires": {
"boolbase": "~1.0.0" "boolbase": "^1.0.0"
} }
}, },
"object-assign": { "object-assign": {
@ -2499,6 +2438,21 @@
"lines-and-columns": "^1.1.6" "lines-and-columns": "^1.1.6"
} }
}, },
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
"parse5-htmlparser2-tree-adapter": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"dev": true,
"requires": {
"parse5": "^6.0.1"
}
},
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -2873,10 +2827,14 @@
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }
}, },
"safe-buffer": { "sade": {
"version": "5.2.1", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==",
"dev": true,
"requires": {
"mri": "^1.1.0"
}
}, },
"sass": { "sass": {
"version": "1.32.8", "version": "1.32.8",
@ -3216,6 +3174,12 @@
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
}, },
"totalist": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-2.0.0.tgz",
"integrity": "sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ==",
"dev": true
},
"touch": { "touch": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@ -3366,6 +3330,19 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}, },
"uvu": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.1.tgz",
"integrity": "sha512-JGxttnOGDFs77FaZ0yMUHIzczzQ5R1IlDeNW6Wymw6gAscwMdAffVOP6TlxLIfReZyK8tahoGwWZaTCJzNFDkg==",
"dev": true,
"requires": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3",
"totalist": "^2.0.0"
}
},
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",

View file

@ -21,7 +21,8 @@
"dev": "concurrently 'tsc --watch' 'npm run copy-js:watch'", "dev": "concurrently 'tsc --watch' 'npm run copy-js:watch'",
"format": "prettier -w 'src/**/*.{js,ts}'", "format": "prettier -w 'src/**/*.{js,ts}'",
"copy-js": "copyfiles -u 1 src/*.js lib/", "copy-js": "copyfiles -u 1 src/*.js lib/",
"copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'" "copy-js:watch": "nodemon -w src --ext js --exec 'npm run copy-js'",
"test": "uvu test -i fixtures -i test-utils.js"
}, },
"dependencies": { "dependencies": {
"@types/estree": "0.0.46", "@types/estree": "0.0.46",
@ -31,8 +32,8 @@
"acorn-jsx": "^5.3.1", "acorn-jsx": "^5.3.1",
"astring": "^1.7.0", "astring": "^1.7.0",
"autoprefixer": "^10.2.5", "autoprefixer": "^10.2.5",
"cheerio": "^0.22.0",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"deepmerge": "^4.2.2",
"domhandler": "^4.0.0", "domhandler": "^4.0.0",
"es-module-lexer": "^0.4.1", "es-module-lexer": "^0.4.1",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
@ -57,6 +58,7 @@
"@types/yargs-parser": "^20.2.0", "@types/yargs-parser": "^20.2.0",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^4.18.0",
"cheerio": "^1.0.0-rc.5",
"concurrently": "^6.0.0", "concurrently": "^6.0.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"eslint": "^7.22.0", "eslint": "^7.22.0",
@ -67,6 +69,7 @@
"preact": "^10.5.12", "preact": "^10.5.12",
"preact-render-to-string": "^5.1.14", "preact-render-to-string": "^5.1.14",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"typescript": "^4.2.3" "typescript": "^4.2.3",
"uvu": "^0.5.1"
} }
} }

View file

@ -1,10 +1,10 @@
import type { AstroConfig } from './@types/astro'; import type { AstroConfig } from './@types/astro';
import * as colors from 'kleur/colors'; import * as colors from 'kleur/colors';
import { join as pathJoin, resolve as pathResolve } from 'path'; import { promises as fsPromises } from 'fs';
import { existsSync, promises as fsPromises } from 'fs';
import yargs from 'yargs-parser'; import yargs from 'yargs-parser';
import { loadConfig } from './config.js';
import generate from './generate.js'; import generate from './generate.js';
import devServer from './dev.js'; import devServer from './dev.js';
@ -49,25 +49,6 @@ async function printVersion() {
console.error(pkg.version); console.error(pkg.version);
} }
async function loadConfig(rawRoot: string | undefined): Promise<AstroConfig | undefined> {
if (typeof rawRoot === 'undefined') {
rawRoot = process.cwd();
}
const root = pathResolve(rawRoot);
const fileProtocolRoot = `file://${root}/`;
const astroConfigPath = pathJoin(root, 'astro.config.mjs');
if (!existsSync(astroConfigPath)) {
return undefined;
}
const astroConfig: AstroConfig = (await import(astroConfigPath)).default;
astroConfig.projectRoot = new URL(astroConfig.projectRoot + '/', fileProtocolRoot);
astroConfig.hmxRoot = new URL(astroConfig.hmxRoot + '/', fileProtocolRoot);
return astroConfig;
}
async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>) { async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void>) {
const astroConfig = await loadConfig(rawRoot); const astroConfig = await loadConfig(rawRoot);
if (typeof astroConfig === 'undefined') { if (typeof astroConfig === 'undefined') {

View file

@ -1,6 +1,6 @@
import type { CompileOptions } from '../@types/compiler'; import type { CompileOptions } from '../@types/compiler';
import type { Ast, TemplateNode } from '../compiler/interfaces'; import type { Ast, TemplateNode } from '../compiler/interfaces';
import type { JsxItem, TransformResult } from '../@types/astro.js'; import type { JsxItem, TransformResult } from '../@types/astro';
import eslexer from 'es-module-lexer'; import eslexer from 'es-module-lexer';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
@ -61,7 +61,6 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(getTextFromAttribute(val)); result[attr.name] = JSON.stringify(getTextFromAttribute(val));
continue; continue;
default: default:
console.log(val);
throw new Error('UNKNOWN V'); throw new Error('UNKNOWN V');
} }
} }
@ -75,7 +74,6 @@ function getTextFromAttribute(attr: any): string {
if (attr.data !== undefined) { if (attr.data !== undefined) {
return attr.data; return attr.data;
} }
console.log(attr);
throw new Error('UNKNOWN attr'); throw new Error('UNKNOWN attr');
} }
@ -169,12 +167,11 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string {
export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> { export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
await eslexer.init; await eslexer.init;
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
// Compile scripts as TypeScript, always // Compile scripts as TypeScript, always
const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx');
// Todo: Validate that `h` and `Fragment` aren't defined in the script // Todo: Validate that `h` and `Fragment` aren't defined in the script
const [scriptImports] = eslexer.parse(script, 'optional-sourcename'); const [scriptImports] = eslexer.parse(script, 'optional-sourcename');
const components = Object.fromEntries( const components = Object.fromEntries(
scriptImports.map((imp) => { scriptImports.map((imp) => {
@ -193,7 +190,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
walk(ast.html, { walk(ast.html, {
enter(node: TemplateNode) { enter(node: TemplateNode) {
// console.log("enter", node.type);
switch (node.type) { switch (node.type) {
case 'MustacheTag': case 'MustacheTag':
let code = compileScriptSafe(node.expression, 'jsx'); let code = compileScriptSafe(node.expression, 'jsx');
@ -238,7 +234,6 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
case 'Element': case 'Element':
const name: string = node.name; const name: string = node.name;
if (!name) { if (!name) {
console.log(node);
throw new Error('AHHHH'); throw new Error('AHHHH');
} }
const attributes = getAttributes(node.attributes); const attributes = getAttributes(node.attributes);
@ -298,12 +293,10 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
return; return;
} }
default: default:
console.log(node);
throw new Error('Unexpected node type: ' + node.type); throw new Error('Unexpected node type: ' + node.type);
} }
}, },
leave(node, parent, prop, index) { leave(node, parent, prop, index) {
// console.log("leave", node.type);
switch (node.type) { switch (node.type) {
case 'Text': case 'Text':
case 'MustacheTag': case 'MustacheTag':

View file

@ -1,75 +1,82 @@
// @ts-nocheck // @ts-nocheck
const now = (typeof process !== 'undefined' && process.hrtime) const now =
? () => { typeof process !== 'undefined' && process.hrtime
const t = process.hrtime(); ? () => {
return t[0] * 1e3 + t[1] / 1e6; const t = process.hrtime();
} return t[0] * 1e3 + t[1] / 1e6;
: () => self.performance.now(); }
: () => self.performance.now();
interface Timing { interface Timing {
label: string; label: string;
start: number; start: number;
end: number; end: number;
children: Timing[]; children: Timing[];
} }
function collapse_timings(timings) { function collapse_timings(timings) {
const result = {}; const result = {};
timings.forEach(timing => { timings.forEach((timing) => {
result[timing.label] = Object.assign({ result[timing.label] = Object.assign(
total: timing.end - timing.start {
}, timing.children && collapse_timings(timing.children)); total: timing.end - timing.start,
}); },
return result; timing.children && collapse_timings(timing.children)
);
});
return result;
} }
export default class Stats { export default class Stats {
start_time: number; start_time: number;
current_timing: Timing; current_timing: Timing;
current_children: Timing[]; current_children: Timing[];
timings: Timing[]; timings: Timing[];
stack: Timing[]; stack: Timing[];
constructor() { constructor() {
this.start_time = now(); this.start_time = now();
this.stack = []; this.stack = [];
this.current_children = this.timings = []; this.current_children = this.timings = [];
} }
start(label) { start(label) {
const timing = { const timing = {
label, label,
start: now(), start: now(),
end: null, end: null,
children: [] children: [],
}; };
this.current_children.push(timing); this.current_children.push(timing);
this.stack.push(timing); this.stack.push(timing);
this.current_timing = timing; this.current_timing = timing;
this.current_children = timing.children; this.current_children = timing.children;
} }
stop(label) { stop(label) {
if (label !== this.current_timing.label) { if (label !== this.current_timing.label) {
throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`); throw new Error(`Mismatched timing labels (expected ${this.current_timing.label}, got ${label})`);
} }
this.current_timing.end = now(); this.current_timing.end = now();
this.stack.pop(); this.stack.pop();
this.current_timing = this.stack[this.stack.length - 1]; this.current_timing = this.stack[this.stack.length - 1];
this.current_children = this.current_timing ? this.current_timing.children : this.timings; this.current_children = this.current_timing ? this.current_timing.children : this.timings;
} }
render() { render() {
const timings = Object.assign({ const timings = Object.assign(
total: now() - this.start_time {
}, collapse_timings(this.timings)); total: now() - this.start_time,
},
collapse_timings(this.timings)
);
return { return {
timings timings,
}; };
} }
} }

View file

@ -5,14 +5,16 @@ import jsx from 'acorn-jsx';
const acornJsx = acorn.Parser.extend(jsx()); const acornJsx = acorn.Parser.extend(jsx());
export const parse = (source: string): Node => acorn.parse(source, { export const parse = (source: string): Node =>
sourceType: 'module', acorn.parse(source, {
ecmaVersion: 2020, sourceType: 'module',
locations: true ecmaVersion: 2020,
}); locations: true,
});
export const parse_expression_at = (source: string, index: number): Node => acornJsx.parseExpressionAt(source, index, { export const parse_expression_at = (source: string, index: number): Node =>
sourceType: 'module', acornJsx.parseExpressionAt(source, index, {
ecmaVersion: 2020, sourceType: 'module',
locations: true ecmaVersion: 2020,
}); locations: true,
});

View file

@ -8,245 +8,257 @@ import full_char_code_at from '../utils/full_char_code_at.js';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js'; import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces.js';
import error from '../utils/error.js'; import error from '../utils/error.js';
type ParserState = (parser: Parser) => (ParserState | void); type ParserState = (parser: Parser) => ParserState | void;
interface LastAutoClosedTag { interface LastAutoClosedTag {
tag: string; tag: string;
reason: string; reason: string;
depth: number; depth: number;
} }
export class Parser { export class Parser {
readonly template: string; readonly template: string;
readonly filename?: string; readonly filename?: string;
readonly customElement: boolean; readonly customElement: boolean;
index = 0; index = 0;
stack: TemplateNode[] = []; stack: TemplateNode[] = [];
html: Fragment; html: Fragment;
css: Style[] = []; css: Style[] = [];
js: Script[] = []; js: Script[] = [];
meta_tags = {}; meta_tags = {};
last_auto_closed_tag?: LastAutoClosedTag; last_auto_closed_tag?: LastAutoClosedTag;
constructor(template: string, options: ParserOptions) { constructor(template: string, options: ParserOptions) {
if (typeof template !== 'string') { if (typeof template !== 'string') {
throw new TypeError('Template must be a string'); throw new TypeError('Template must be a string');
} }
this.template = template.replace(/\s+$/, ''); this.template = template.replace(/\s+$/, '');
this.filename = options.filename; this.filename = options.filename;
this.customElement = options.customElement; this.customElement = options.customElement;
this.html = { this.html = {
start: null, start: null,
end: null, end: null,
type: 'Fragment', type: 'Fragment',
children: [] children: [],
}; };
this.stack.push(this.html); this.stack.push(this.html);
let state: ParserState = fragment; let state: ParserState = fragment;
while (this.index < this.template.length) { while (this.index < this.template.length) {
state = state(this) || fragment; state = state(this) || fragment;
} }
if (this.stack.length > 1) { if (this.stack.length > 1) {
const current = this.current(); const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block'; const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
const slug = current.type === 'Element' ? 'element' : 'block'; const slug = current.type === 'Element' ? 'element' : 'block';
this.error({ this.error(
code: `unclosed-${slug}`, {
message: `${type} was left open` code: `unclosed-${slug}`,
}, current.start); message: `${type} was left open`,
} },
current.start
);
}
if (state !== fragment) { if (state !== fragment) {
this.error({ this.error({
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input',
}); });
} }
if (this.html.children.length) { if (this.html.children.length) {
let start = this.html.children[0].start; let start = this.html.children[0].start;
while (whitespace.test(template[start])) start += 1; while (whitespace.test(template[start])) start += 1;
let end = this.html.children[this.html.children.length - 1].end; let end = this.html.children[this.html.children.length - 1].end;
while (whitespace.test(template[end - 1])) end -= 1; while (whitespace.test(template[end - 1])) end -= 1;
this.html.start = start; this.html.start = start;
this.html.end = end; this.html.end = end;
} else { } else {
this.html.start = this.html.end = null; this.html.start = this.html.end = null;
} }
} }
current() { current() {
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
acorn_error(err: any) { acorn_error(err: any) {
this.error({ this.error(
code: 'parse-error', {
message: err.message.replace(/ \(\d+:\d+\)$/, '') code: 'parse-error',
}, err.pos); message: err.message.replace(/ \(\d+:\d+\)$/, ''),
} },
err.pos
);
}
error({ code, message }: { code: string; message: string }, index = this.index) { error({ code, message }: { code: string; message: string }, index = this.index) {
error(message, { error(message, {
name: 'ParseError', name: 'ParseError',
code, code,
source: this.template, source: this.template,
start: index, start: index,
filename: this.filename filename: this.filename,
}); });
} }
eat(str: string, required?: boolean, message?: string) { eat(str: string, required?: boolean, message?: string) {
if (this.match(str)) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;
} }
if (required) { if (required) {
this.error({ this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`, code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}` message: message || `Expected ${str}`,
}); });
} }
return false; return false;
} }
match(str: string) { match(str: string) {
return this.template.slice(this.index, this.index + str.length) === str; return this.template.slice(this.index, this.index + str.length) === str;
} }
match_regex(pattern: RegExp) { match_regex(pattern: RegExp) {
const match = pattern.exec(this.template.slice(this.index)); const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null; if (!match || match.index !== 0) return null;
return match[0]; return match[0];
} }
allow_whitespace() { allow_whitespace() {
while ( while (this.index < this.template.length && whitespace.test(this.template[this.index])) {
this.index < this.template.length && this.index++;
whitespace.test(this.template[this.index]) }
) { }
this.index++;
}
}
read(pattern: RegExp) { read(pattern: RegExp) {
const result = this.match_regex(pattern); const result = this.match_regex(pattern);
if (result) this.index += result.length; if (result) this.index += result.length;
return result; return result;
} }
read_identifier(allow_reserved = false) { read_identifier(allow_reserved = false) {
const start = this.index; const start = this.index;
let i = this.index; let i = this.index;
const code = full_char_code_at(this.template, i); const code = full_char_code_at(this.template, i);
if (!isIdentifierStart(code, true)) return null; if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) { while (i < this.template.length) {
const code = full_char_code_at(this.template, i); const code = full_char_code_at(this.template, i);
if (!isIdentifierChar(code, true)) break; if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
} }
const identifier = this.template.slice(this.index, this.index = i); const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.has(identifier)) { if (!allow_reserved && reserved.has(identifier)) {
this.error({ this.error(
code: 'unexpected-reserved-word', {
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here` code: 'unexpected-reserved-word',
}, start); message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`,
} },
start
);
}
return identifier; return identifier;
} }
read_until(pattern: RegExp) { read_until(pattern: RegExp) {
if (this.index >= this.template.length) { if (this.index >= this.template.length) {
this.error({ this.error({
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input',
}); });
} }
const start = this.index; const start = this.index;
const match = pattern.exec(this.template.slice(start)); const match = pattern.exec(this.template.slice(start));
if (match) { if (match) {
this.index = start + match.index; this.index = start + match.index;
return this.template.slice(start, this.index); return this.template.slice(start, this.index);
} }
this.index = this.template.length; this.index = this.template.length;
return this.template.slice(start); return this.template.slice(start);
} }
require_whitespace() { require_whitespace() {
if (!whitespace.test(this.template[this.index])) { if (!whitespace.test(this.template[this.index])) {
this.error({ this.error({
code: 'missing-whitespace', code: 'missing-whitespace',
message: 'Expected whitespace' message: 'Expected whitespace',
}); });
} }
this.allow_whitespace(); this.allow_whitespace();
} }
} }
export default function parse( export default function parse(template: string, options: ParserOptions = {}): Ast {
template: string, const parser = new Parser(template, options);
options: ParserOptions = {}
): Ast {
const parser = new Parser(template, options);
// TODO we may want to allow multiple <style> tags — // TODO we may want to allow multiple <style> tags —
// one scoped, one global. for now, only allow one // one scoped, one global. for now, only allow one
if (parser.css.length > 1) { if (parser.css.length > 1) {
parser.error({ parser.error(
code: 'duplicate-style', {
message: 'You can only have one top-level <style> tag per component' code: 'duplicate-style',
}, parser.css[1].start); message: 'You can only have one top-level <style> tag per component',
} },
parser.css[1].start
);
}
const instance_scripts = parser.js.filter(script => script.context === 'default'); const instance_scripts = parser.js.filter((script) => script.context === 'default');
const module_scripts = parser.js.filter(script => script.context === 'module'); const module_scripts = parser.js.filter((script) => script.context === 'module');
if (instance_scripts.length > 1) { if (instance_scripts.length > 1) {
parser.error({ parser.error(
code: 'invalid-script', {
message: 'A component can only have one instance-level <script> element' code: 'invalid-script',
}, instance_scripts[1].start); message: 'A component can only have one instance-level <script> element',
} },
instance_scripts[1].start
);
}
if (module_scripts.length > 1) { if (module_scripts.length > 1) {
parser.error({ parser.error(
code: 'invalid-script', {
message: 'A component can only have one <script context="module"> element' code: 'invalid-script',
}, module_scripts[1].start); message: 'A component can only have one <script context="module"> element',
} },
module_scripts[1].start
);
}
return { return {
html: parser.html, html: parser.html,
css: parser.css[0], css: parser.css[0],
instance: instance_scripts[0], instance: instance_scripts[0],
module: module_scripts[0] module: module_scripts[0],
}; };
} }

View file

@ -3,82 +3,70 @@
import { Parser } from '../index.js'; import { Parser } from '../index.js';
import { isIdentifierStart } from 'acorn'; import { isIdentifierStart } from 'acorn';
import full_char_code_at from '../../utils/full_char_code_at.js'; import full_char_code_at from '../../utils/full_char_code_at.js';
import { import { is_bracket_open, is_bracket_close, is_bracket_pair, get_bracket_close } from '../utils/bracket.js';
is_bracket_open,
is_bracket_close,
is_bracket_pair,
get_bracket_close
} from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js'; import { parse_expression_at } from '../acorn.js';
import { Pattern } from 'estree'; import { Pattern } from 'estree';
export default function read_context( export default function read_context(parser: Parser): Pattern & { start: number; end: number } {
parser: Parser const start = parser.index;
): Pattern & { start: number; end: number } { let i = parser.index;
const start = parser.index;
let i = parser.index;
const code = full_char_code_at(parser.template, i); const code = full_char_code_at(parser.template, i);
if (isIdentifierStart(code, true)) { if (isIdentifierStart(code, true)) {
return { return {
type: 'Identifier', type: 'Identifier',
name: parser.read_identifier(), name: parser.read_identifier(),
start, start,
end: parser.index end: parser.index,
}; };
} }
if (!is_bracket_open(code)) { if (!is_bracket_open(code)) {
parser.error({ parser.error({
code: 'unexpected-token', code: 'unexpected-token',
message: 'Expected identifier or destructure pattern' message: 'Expected identifier or destructure pattern',
}); });
} }
const bracket_stack = [code]; const bracket_stack = [code];
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
while (i < parser.template.length) { while (i < parser.template.length) {
const code = full_char_code_at(parser.template, i); const code = full_char_code_at(parser.template, i);
if (is_bracket_open(code)) { if (is_bracket_open(code)) {
bracket_stack.push(code); bracket_stack.push(code);
} else if (is_bracket_close(code)) { } else if (is_bracket_close(code)) {
if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) { if (!is_bracket_pair(bracket_stack[bracket_stack.length - 1], code)) {
parser.error({ parser.error({
code: 'unexpected-token', code: 'unexpected-token',
message: `Expected ${String.fromCharCode( message: `Expected ${String.fromCharCode(get_bracket_close(bracket_stack[bracket_stack.length - 1]))}`,
get_bracket_close(bracket_stack[bracket_stack.length - 1]) });
)}` }
}); bracket_stack.pop();
} if (bracket_stack.length === 0) {
bracket_stack.pop(); i += code <= 0xffff ? 1 : 2;
if (bracket_stack.length === 0) { break;
i += code <= 0xffff ? 1 : 2; }
break; }
} i += code <= 0xffff ? 1 : 2;
} }
i += code <= 0xffff ? 1 : 2;
}
parser.index = i; parser.index = i;
const pattern_string = parser.template.slice(start, i); const pattern_string = parser.template.slice(start, i);
try { try {
// the length of the `space_with_newline` has to be start - 1 // the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string, // because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1 // which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline` // so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered, // to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node // so it will not affect the `column` of the node
let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' '); let space_with_newline = parser.template.slice(0, start).replace(/[^\n]/g, ' ');
const first_space = space_with_newline.indexOf(' '); const first_space = space_with_newline.indexOf(' ');
space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
return (parse_expression_at( return (parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) as any).left;
`${space_with_newline}(${pattern_string} = 1)`, } catch (error) {
start - 1 parser.acorn_error(error);
) as any).left; }
} catch (error) {
parser.acorn_error(error);
}
} }

View file

@ -6,36 +6,39 @@ import { whitespace } from '../../utils/patterns.js';
// import { Node } from 'estree'; // import { Node } from 'estree';
export default function read_expression(parser: Parser): string { export default function read_expression(parser: Parser): string {
try { try {
const node = parse_expression_at(parser.template, parser.index); const node = parse_expression_at(parser.template, parser.index);
let num_parens = 0; let num_parens = 0;
for (let i = parser.index; i < node.start; i += 1) { for (let i = parser.index; i < node.start; i += 1) {
if (parser.template[i] === '(') num_parens += 1; if (parser.template[i] === '(') num_parens += 1;
} }
let index = node.end; let index = node.end;
while (num_parens > 0) { while (num_parens > 0) {
const char = parser.template[index]; const char = parser.template[index];
if (char === ')') { if (char === ')') {
num_parens -= 1; num_parens -= 1;
} else if (!whitespace.test(char)) { } else if (!whitespace.test(char)) {
parser.error({ parser.error(
code: 'unexpected-token', {
message: 'Expected )' code: 'unexpected-token',
}, index); message: 'Expected )',
} },
index
);
}
index += 1; index += 1;
} }
parser.index = index; parser.index = index;
return parser.template.substring(node.start, node.end); return parser.template.substring(node.start, node.end);
// return node as Node; // return node as Node;
} catch (err) { } catch (err) {
parser.acorn_error(err); parser.acorn_error(err);
} }
} }

View file

@ -8,48 +8,53 @@ import { Node, Program } from 'estree';
const script_closing_tag = '</script>'; const script_closing_tag = '</script>';
function get_context(parser: Parser, attributes: any[], start: number): string { function get_context(parser: Parser, attributes: any[], start: number): string {
const context = attributes.find(attribute => attribute.name === 'context'); const context = attributes.find((attribute) => attribute.name === 'context');
if (!context) return 'default'; if (!context) return 'default';
if (context.value.length !== 1 || context.value[0].type !== 'Text') { if (context.value.length !== 1 || context.value[0].type !== 'Text') {
parser.error({ parser.error(
code: 'invalid-script', {
message: 'context attribute must be static' code: 'invalid-script',
}, start); message: 'context attribute must be static',
} },
start
);
}
const value = context.value[0].data; const value = context.value[0].data;
if (value !== 'module') { if (value !== 'module') {
parser.error({ parser.error(
code: 'invalid-script', {
message: 'If the context attribute is supplied, its value must be "module"' code: 'invalid-script',
}, context.start); message: 'If the context attribute is supplied, its value must be "module"',
} },
context.start
);
}
return value; return value;
} }
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script { export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
const script_start = parser.index; const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start); const script_end = parser.template.indexOf(script_closing_tag, script_start);
if (script_end === -1) { if (script_end === -1) {
parser.error({ parser.error({
code: 'unclosed-script', code: 'unclosed-script',
message: '<script> must have a closing tag' message: '<script> must have a closing tag',
}); });
} }
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + parser.template.slice(script_start, script_end);
parser.template.slice(script_start, script_end); parser.index = script_end + script_closing_tag.length;
parser.index = script_end + script_closing_tag.length;
return { return {
type: 'Script', type: 'Script',
start, start,
end: parser.index, end: parser.index,
context: get_context(parser, attributes, start), context: get_context(parser, attributes, start),
content: source content: source,
}; };
} }

View file

@ -4,13 +4,13 @@ import text from './text.js';
import { Parser } from '../index.js'; import { Parser } from '../index.js';
export default function fragment(parser: Parser) { export default function fragment(parser: Parser) {
if (parser.match('<')) { if (parser.match('<')) {
return tag; return tag;
} }
if (parser.match('{')) { if (parser.match('{')) {
return mustache; return mustache;
} }
return text; return text;
} }

View file

@ -10,403 +10,404 @@ import { TemplateNode } from '../../interfaces.js';
type TODO = any; type TODO = any;
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) { function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
if (!block.children || block.children.length === 0) return; // AwaitBlock if (!block.children || block.children.length === 0) return; // AwaitBlock
const first_child = block.children[0]; const first_child = block.children[0];
const last_child = block.children[block.children.length - 1]; const last_child = block.children[block.children.length - 1];
if (first_child.type === 'Text' && trim_before) { if (first_child.type === 'Text' && trim_before) {
first_child.data = trim_start(first_child.data); first_child.data = trim_start(first_child.data);
if (!first_child.data) block.children.shift(); if (!first_child.data) block.children.shift();
} }
if (last_child.type === 'Text' && trim_after) { if (last_child.type === 'Text' && trim_after) {
last_child.data = trim_end(last_child.data); last_child.data = trim_end(last_child.data);
if (!last_child.data) block.children.pop(); if (!last_child.data) block.children.pop();
} }
if (block.else) { if (block.else) {
trim_whitespace(block.else, trim_before, trim_after); trim_whitespace(block.else, trim_before, trim_after);
} }
if (first_child.elseif) { if (first_child.elseif) {
trim_whitespace(first_child, trim_before, trim_after); trim_whitespace(first_child, trim_before, trim_after);
} }
} }
export default function mustache(parser: Parser) { export default function mustache(parser: Parser) {
const start = parser.index; const start = parser.index;
parser.index += 1; parser.index += 1;
parser.allow_whitespace(); parser.allow_whitespace();
// {/if}, {/each}, {/await} or {/key} // {/if}, {/each}, {/await} or {/key}
if (parser.eat('/')) { if (parser.eat('/')) {
let block = parser.current(); let block = parser.current();
let expected: TODO; let expected: TODO;
if (closing_tag_omitted(block.name)) { if (closing_tag_omitted(block.name)) {
block.end = start; block.end = start;
parser.stack.pop(); parser.stack.pop();
block = parser.current(); block = parser.current();
} }
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
block.end = start; block.end = start;
parser.stack.pop(); parser.stack.pop();
block = parser.current(); block = parser.current();
expected = 'await'; expected = 'await';
} }
if (block.type === 'IfBlock') { if (block.type === 'IfBlock') {
expected = 'if'; expected = 'if';
} else if (block.type === 'EachBlock') { } else if (block.type === 'EachBlock') {
expected = 'each'; expected = 'each';
} else if (block.type === 'AwaitBlock') { } else if (block.type === 'AwaitBlock') {
expected = 'await'; expected = 'await';
} else if (block.type === 'KeyBlock') { } else if (block.type === 'KeyBlock') {
expected = 'key'; expected = 'key';
} else { } else {
parser.error({ parser.error({
code: 'unexpected-block-close', code: 'unexpected-block-close',
message: 'Unexpected block closing tag' message: 'Unexpected block closing tag',
}); });
} }
parser.eat(expected, true); parser.eat(expected, true);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
while (block.elseif) { while (block.elseif) {
block.end = parser.index; block.end = parser.index;
parser.stack.pop(); parser.stack.pop();
block = parser.current(); block = parser.current();
if (block.else) { if (block.else) {
block.else.end = start; block.else.end = start;
} }
} }
// strip leading/trailing whitespace as necessary // strip leading/trailing whitespace as necessary
const char_before = parser.template[block.start - 1]; const char_before = parser.template[block.start - 1];
const char_after = parser.template[parser.index]; const char_after = parser.template[parser.index];
const trim_before = !char_before || whitespace.test(char_before); const trim_before = !char_before || whitespace.test(char_before);
const trim_after = !char_after || whitespace.test(char_after); const trim_after = !char_after || whitespace.test(char_after);
trim_whitespace(block, trim_before, trim_after); trim_whitespace(block, trim_before, trim_after);
block.end = parser.index; block.end = parser.index;
parser.stack.pop(); parser.stack.pop();
} else if (parser.eat(':else')) { } else if (parser.eat(':else')) {
if (parser.eat('if')) { if (parser.eat('if')) {
parser.error({ parser.error({
code: 'invalid-elseif', code: 'invalid-elseif',
message: "'elseif' should be 'else if'" message: "'elseif' should be 'else if'",
}); });
} }
parser.allow_whitespace(); parser.allow_whitespace();
// :else if // :else if
if (parser.eat('if')) { if (parser.eat('if')) {
const block = parser.current(); const block = parser.current();
if (block.type !== 'IfBlock') { if (block.type !== 'IfBlock') {
parser.error({ parser.error({
code: 'invalid-elseif-placement', code: 'invalid-elseif-placement',
message: parser.stack.some(block => block.type === 'IfBlock') message: parser.stack.some((block) => block.type === 'IfBlock')
? `Expected to close ${to_string(block)} before seeing {:else if ...} block` ? `Expected to close ${to_string(block)} before seeing {:else if ...} block`
: 'Cannot have an {:else if ...} block outside an {#if ...} block' : 'Cannot have an {:else if ...} block outside an {#if ...} block',
}); });
} }
parser.require_whitespace(); parser.require_whitespace();
const expression = read_expression(parser); const expression = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
block.else = { block.else = {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'ElseBlock', type: 'ElseBlock',
children: [ children: [
{ {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'IfBlock', type: 'IfBlock',
elseif: true, elseif: true,
expression, expression,
children: [] children: [],
} },
] ],
}; };
parser.stack.push(block.else.children[0]); parser.stack.push(block.else.children[0]);
} else { } else {
// :else // :else
const block = parser.current(); const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({ parser.error({
code: 'invalid-else-placement', code: 'invalid-else-placement',
message: parser.stack.some(block => block.type === 'IfBlock' || block.type === 'EachBlock') message: parser.stack.some((block) => block.type === 'IfBlock' || block.type === 'EachBlock')
? `Expected to close ${to_string(block)} before seeing {:else} block` ? `Expected to close ${to_string(block)} before seeing {:else} block`
: 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block' : 'Cannot have an {:else} block outside an {#if ...} or {#each ...} block',
}); });
} }
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
block.else = { block.else = {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'ElseBlock', type: 'ElseBlock',
children: [] children: [],
}; };
parser.stack.push(block.else); parser.stack.push(block.else);
} }
} else if (parser.match(':then') || parser.match(':catch')) { } else if (parser.match(':then') || parser.match(':catch')) {
const block = parser.current(); const block = parser.current();
const is_then = parser.eat(':then') || !parser.eat(':catch'); const is_then = parser.eat(':then') || !parser.eat(':catch');
if (is_then) { if (is_then) {
if (block.type !== 'PendingBlock') { if (block.type !== 'PendingBlock') {
parser.error({ parser.error({
code: 'invalid-then-placement', code: 'invalid-then-placement',
message: parser.stack.some(block => block.type === 'PendingBlock') message: parser.stack.some((block) => block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:then} block` ? `Expected to close ${to_string(block)} before seeing {:then} block`
: 'Cannot have an {:then} block outside an {#await ...} block' : 'Cannot have an {:then} block outside an {#await ...} block',
}); });
} }
} else { } else {
if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') { if (block.type !== 'ThenBlock' && block.type !== 'PendingBlock') {
parser.error({ parser.error({
code: 'invalid-catch-placement', code: 'invalid-catch-placement',
message: parser.stack.some(block => block.type === 'ThenBlock' || block.type === 'PendingBlock') message: parser.stack.some((block) => block.type === 'ThenBlock' || block.type === 'PendingBlock')
? `Expected to close ${to_string(block)} before seeing {:catch} block` ? `Expected to close ${to_string(block)} before seeing {:catch} block`
: 'Cannot have an {:catch} block outside an {#await ...} block' : 'Cannot have an {:catch} block outside an {#await ...} block',
}); });
} }
} }
block.end = start; block.end = start;
parser.stack.pop(); parser.stack.pop();
const await_block = parser.current(); const await_block = parser.current();
if (!parser.eat('}')) { if (!parser.eat('}')) {
parser.require_whitespace(); parser.require_whitespace();
await_block[is_then ? 'value' : 'error'] = read_context(parser); await_block[is_then ? 'value' : 'error'] = read_context(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
} }
const new_block: TemplateNode = { const new_block: TemplateNode = {
start, start,
// @ts-ignore // @ts-ignore
end: null, end: null,
type: is_then ? 'ThenBlock' : 'CatchBlock', type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [], children: [],
skip: false skip: false,
}; };
await_block[is_then ? 'then' : 'catch'] = new_block; await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block); parser.stack.push(new_block);
} else if (parser.eat('#')) { } else if (parser.eat('#')) {
// {#if foo}, {#each foo} or {#await foo} // {#if foo}, {#each foo} or {#await foo}
let type; let type;
if (parser.eat('if')) { if (parser.eat('if')) {
type = 'IfBlock'; type = 'IfBlock';
} else if (parser.eat('each')) { } else if (parser.eat('each')) {
type = 'EachBlock'; type = 'EachBlock';
} else if (parser.eat('await')) { } else if (parser.eat('await')) {
type = 'AwaitBlock'; type = 'AwaitBlock';
} else if (parser.eat('key')) { } else if (parser.eat('key')) {
type = 'KeyBlock'; type = 'KeyBlock';
} else { } else {
parser.error({ parser.error({
code: 'expected-block-type', code: 'expected-block-type',
message: 'Expected if, each, await or key' message: 'Expected if, each, await or key',
}); });
} }
parser.require_whitespace(); parser.require_whitespace();
const expression = read_expression(parser); const expression = read_expression(parser);
// @ts-ignore // @ts-ignore
const block: TemplateNode = type === 'AwaitBlock' ? const block: TemplateNode =
{ type === 'AwaitBlock'
start, ? {
end: null, start,
type, end: null,
expression, type,
value: null, expression,
error: null, value: null,
pending: { error: null,
start: null, pending: {
end: null, start: null,
type: 'PendingBlock', end: null,
children: [], type: 'PendingBlock',
skip: true children: [],
}, skip: true,
then: { },
start: null, then: {
end: null, start: null,
type: 'ThenBlock', end: null,
children: [], type: 'ThenBlock',
skip: true children: [],
}, skip: true,
catch: { },
start: null, catch: {
end: null, start: null,
type: 'CatchBlock', end: null,
children: [], type: 'CatchBlock',
skip: true children: [],
} skip: true,
} : },
{ }
start, : {
end: null, start,
type, end: null,
expression, type,
children: [] expression,
}; children: [],
};
parser.allow_whitespace(); parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item} // {#each} blocks must declare a context {#each list as item}
if (type === 'EachBlock') { if (type === 'EachBlock') {
parser.eat('as', true); parser.eat('as', true);
parser.require_whitespace(); parser.require_whitespace();
block.context = read_context(parser); block.context = read_context(parser);
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.eat(',')) { if (parser.eat(',')) {
parser.allow_whitespace(); parser.allow_whitespace();
block.index = parser.read_identifier(); block.index = parser.read_identifier();
if (!block.index) { if (!block.index) {
parser.error({ parser.error({
code: 'expected-name', code: 'expected-name',
message: 'Expected name' message: 'Expected name',
}); });
} }
parser.allow_whitespace(); parser.allow_whitespace();
} }
if (parser.eat('(')) { if (parser.eat('(')) {
parser.allow_whitespace(); parser.allow_whitespace();
block.key = read_expression(parser); block.key = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat(')', true); parser.eat(')', true);
parser.allow_whitespace(); parser.allow_whitespace();
} }
} }
const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then'); const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) { if (await_block_shorthand) {
parser.require_whitespace(); parser.require_whitespace();
block.value = read_context(parser); block.value = read_context(parser);
parser.allow_whitespace(); parser.allow_whitespace();
} }
const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch'); const await_block_catch_shorthand = !await_block_shorthand && type === 'AwaitBlock' && parser.eat('catch');
if (await_block_catch_shorthand) { if (await_block_catch_shorthand) {
parser.require_whitespace(); parser.require_whitespace();
block.error = read_context(parser); block.error = read_context(parser);
parser.allow_whitespace(); parser.allow_whitespace();
} }
parser.eat('}', true); parser.eat('}', true);
// @ts-ignore // @ts-ignore
parser.current().children.push(block); parser.current().children.push(block);
parser.stack.push(block); parser.stack.push(block);
if (type === 'AwaitBlock') { if (type === 'AwaitBlock') {
let child_block; let child_block;
if (await_block_shorthand) { if (await_block_shorthand) {
block.then.skip = false; block.then.skip = false;
child_block = block.then; child_block = block.then;
} else if (await_block_catch_shorthand) { } else if (await_block_catch_shorthand) {
block.catch.skip = false; block.catch.skip = false;
child_block = block.catch; child_block = block.catch;
} else { } else {
block.pending.skip = false; block.pending.skip = false;
child_block = block.pending; child_block = block.pending;
} }
child_block.start = parser.index; child_block.start = parser.index;
parser.stack.push(child_block); parser.stack.push(child_block);
} }
} else if (parser.eat('@html')) { } else if (parser.eat('@html')) {
// {@html content} tag // {@html content} tag
parser.require_whitespace(); parser.require_whitespace();
const expression = read_expression(parser); const expression = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
// @ts-ignore // @ts-ignore
parser.current().children.push({ parser.current().children.push({
start, start,
end: parser.index, end: parser.index,
type: 'RawMustacheTag', type: 'RawMustacheTag',
expression expression,
}); });
} else if (parser.eat('@debug')) { } else if (parser.eat('@debug')) {
// let identifiers; // let identifiers;
// // Implies {@debug} which indicates "debug all" // // Implies {@debug} which indicates "debug all"
// if (parser.read(/\s*}/)) { // if (parser.read(/\s*}/)) {
// identifiers = []; // identifiers = [];
// } else { // } else {
// const expression = read_expression(parser); // const expression = read_expression(parser);
// identifiers = expression.type === 'SequenceExpression' // identifiers = expression.type === 'SequenceExpression'
// ? expression.expressions // ? expression.expressions
// : [expression]; // : [expression];
// identifiers.forEach(node => { // identifiers.forEach(node => {
// if (node.type !== 'Identifier') { // if (node.type !== 'Identifier') {
// parser.error({ // parser.error({
// code: 'invalid-debug-args', // code: 'invalid-debug-args',
// message: '{@debug ...} arguments must be identifiers, not arbitrary expressions' // message: '{@debug ...} arguments must be identifiers, not arbitrary expressions'
// }, node.start); // }, node.start);
// } // }
// }); // });
// parser.allow_whitespace(); // parser.allow_whitespace();
// parser.eat('}', true); // parser.eat('}', true);
// } // }
// parser.current().children.push({ // parser.current().children.push({
// start, // start,
// end: parser.index, // end: parser.index,
// type: 'DebugTag', // type: 'DebugTag',
// identifiers // identifiers
// }); // });
throw new Error('@debug not yet supported'); throw new Error('@debug not yet supported');
} else { } else {
const expression = read_expression(parser); const expression = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
// @ts-ignore // @ts-ignore
parser.current().children.push({ parser.current().children.push({
start, start,
end: parser.index, end: parser.index,
type: 'MustacheTag', type: 'MustacheTag',
expression expression,
}); });
} }
} }

View file

@ -14,29 +14,29 @@ import list from '../../utils/list.js';
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const meta_tags = new Map([ const meta_tags = new Map([
['svelte:head', 'Head'], ['svelte:head', 'Head'],
['svelte:options', 'Options'], ['svelte:options', 'Options'],
['svelte:window', 'Window'], ['svelte:window', 'Window'],
['svelte:body', 'Body'] ['svelte:body', 'Body'],
]); ]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment'); const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
const specials = new Map([ const specials = new Map([
[ [
'script', 'script',
{ {
read: read_script, read: read_script,
property: 'js' property: 'js',
} },
], ],
[ [
'style', 'style',
{ {
read: read_style, read: read_style,
property: 'css' property: 'css',
} },
] ],
]); ]);
const SELF = /^svelte:self(?=[\s/>])/; const SELF = /^svelte:self(?=[\s/>])/;
@ -44,489 +44,531 @@ const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/; const SLOT = /^svelte:fragment(?=[\s/>])/;
function parent_is_head(stack) { function parent_is_head(stack) {
let i = stack.length; let i = stack.length;
while (i--) { while (i--) {
const { type } = stack[i]; const { type } = stack[i];
if (type === 'Head') return true; if (type === 'Head') return true;
if (type === 'Element' || type === 'InlineComponent') return false; if (type === 'Element' || type === 'InlineComponent') return false;
} }
return false; return false;
} }
export default function tag(parser: Parser) { export default function tag(parser: Parser) {
const start = parser.index++; const start = parser.index++;
let parent = parser.current(); let parent = parser.current();
if (parser.eat('!--')) { if (parser.eat('!--')) {
const data = parser.read_until(/-->/); const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->'); parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({ parser.current().children.push({
start, start,
end: parser.index, end: parser.index,
type: 'Comment', type: 'Comment',
data data,
}); });
return; return;
} }
const is_closing_tag = parser.eat('/'); const is_closing_tag = parser.eat('/');
const name = read_tag_name(parser); const name = read_tag_name(parser);
if (meta_tags.has(name)) { if (meta_tags.has(name)) {
const slug = meta_tags.get(name).toLowerCase(); const slug = meta_tags.get(name).toLowerCase();
if (is_closing_tag) { if (is_closing_tag) {
if ( if ((name === 'svelte:window' || name === 'svelte:body') && parser.current().children.length) {
(name === 'svelte:window' || name === 'svelte:body') && parser.error(
parser.current().children.length {
) { code: `invalid-${slug}-content`,
parser.error({ message: `<${name}> cannot have children`,
code: `invalid-${slug}-content`, },
message: `<${name}> cannot have children` parser.current().children[0].start
}, parser.current().children[0].start); );
} }
} else { } else {
if (name in parser.meta_tags) { if (name in parser.meta_tags) {
parser.error({ parser.error(
code: `duplicate-${slug}`, {
message: `A component can only have one <${name}> tag` code: `duplicate-${slug}`,
}, start); message: `A component can only have one <${name}> tag`,
} },
start
);
}
if (parser.stack.length > 1) { if (parser.stack.length > 1) {
parser.error({ parser.error(
code: `invalid-${slug}-placement`, {
message: `<${name}> tags cannot be inside elements or blocks` code: `invalid-${slug}-placement`,
}, start); message: `<${name}> tags cannot be inside elements or blocks`,
} },
start
);
}
parser.meta_tags[name] = true; parser.meta_tags[name] = true;
} }
} }
const type = meta_tags.has(name) const type = meta_tags.has(name)
? meta_tags.get(name) ? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent' : /[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
: name === 'svelte:fragment' ? 'SlotTemplate' ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title' : name === 'svelte:fragment'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element'; ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack)
? 'Title'
: name === 'slot' && !parser.customElement
? 'Slot'
: 'Element';
const element: TemplateNode = { const element: TemplateNode = {
start, start,
end: null, // filled in later end: null, // filled in later
type, type,
name, name,
attributes: [], attributes: [],
children: [] children: [],
}; };
parser.allow_whitespace(); parser.allow_whitespace();
if (is_closing_tag) { if (is_closing_tag) {
if (is_void(name)) { if (is_void(name)) {
parser.error({ parser.error(
code: 'invalid-void-content', {
message: `<${name}> is a void element and cannot have children, or a closing tag` code: 'invalid-void-content',
}, start); message: `<${name}> is a void element and cannot have children, or a closing tag`,
} },
start
);
}
parser.eat('>', true); parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div> // close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) { while (parent.name !== name) {
if (parent.type !== 'Element') { if (parent.type !== 'Element') {
const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name const message =
? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>` parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name
: `</${name}> attempted to close an element that was not open`; ? `</${name}> attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>`
parser.error({ : `</${name}> attempted to close an element that was not open`;
code: 'invalid-closing-tag', parser.error(
message {
}, start); code: 'invalid-closing-tag',
} message,
},
start
);
}
parent.end = start; parent.end = start;
parser.stack.pop(); parser.stack.pop();
parent = parser.current(); parent = parser.current();
} }
parent.end = parser.index; parent.end = parser.index;
parser.stack.pop(); parser.stack.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) { if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = null; parser.last_auto_closed_tag = null;
} }
return; return;
} else if (closing_tag_omitted(parent.name, name)) { } else if (closing_tag_omitted(parent.name, name)) {
parent.end = start; parent.end = start;
parser.stack.pop(); parser.stack.pop();
parser.last_auto_closed_tag = { parser.last_auto_closed_tag = {
tag: parent.name, tag: parent.name,
reason: name, reason: name,
depth: parser.stack.length depth: parser.stack.length,
}; };
} }
const unique_names: Set<string> = new Set(); const unique_names: Set<string> = new Set();
let attribute; let attribute;
while ((attribute = read_attribute(parser, unique_names))) { while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute); element.attributes.push(attribute);
parser.allow_whitespace(); parser.allow_whitespace();
} }
if (name === 'svelte:component') { if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this'); const index = element.attributes.findIndex((attr) => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) { if (!~index) {
parser.error({ parser.error(
code: 'missing-component-definition', {
message: "<svelte:component> must have a 'this' attribute" code: 'missing-component-definition',
}, start); message: "<svelte:component> must have a 'this' attribute",
} },
start
);
}
const definition = element.attributes.splice(index, 1)[0]; const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') { if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({ parser.error(
code: 'invalid-component-definition', {
message: 'invalid component definition' code: 'invalid-component-definition',
}, definition.start); message: 'invalid component definition',
} },
definition.start
);
}
element.expression = definition.value[0].expression; element.expression = definition.value[0].expression;
} }
// special cases top-level <script> and <style> // special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) { if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name); const special = specials.get(name);
parser.eat('>', true); parser.eat('>', true);
const content = special.read(parser, start, element.attributes); const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content); if (content) parser[special.property].push(content);
return; return;
} }
parser.current().children.push(element); parser.current().children.push(element);
const self_closing = parser.eat('/') || is_void(name); const self_closing = parser.eat('/') || is_void(name);
parser.eat('>', true); parser.eat('>', true);
if (self_closing) { if (self_closing) {
// don't push self-closing elements onto the stack // don't push self-closing elements onto the stack
element.end = parser.index; element.end = parser.index;
} else if (name === 'textarea') { } else if (name === 'textarea') {
// special case // special case
element.children = read_sequence( element.children = read_sequence(parser, () => parser.template.slice(parser.index, parser.index + 11) === '</textarea>');
parser, parser.read(/<\/textarea>/);
() => element.end = parser.index;
parser.template.slice(parser.index, parser.index + 11) === '</textarea>' } else if (name === 'script' || name === 'style') {
); // special case
parser.read(/<\/textarea>/); const start = parser.index;
element.end = parser.index; const data = parser.read_until(new RegExp(`</${name}>`));
} else if (name === 'script' || name === 'style') { const end = parser.index;
// special case element.children.push({ start, end, type: 'Text', data });
const start = parser.index; parser.eat(`</${name}>`, true);
const data = parser.read_until(new RegExp(`</${name}>`)); element.end = parser.index;
const end = parser.index; } else {
element.children.push({ start, end, type: 'Text', data }); parser.stack.push(element);
parser.eat(`</${name}>`, true); }
element.end = parser.index;
} else {
parser.stack.push(element);
}
} }
function read_tag_name(parser: Parser) { function read_tag_name(parser: Parser) {
const start = parser.index; const start = parser.index;
if (parser.read(SELF)) { if (parser.read(SELF)) {
// check we're inside a block, otherwise this // check we're inside a block, otherwise this
// will cause infinite recursion // will cause infinite recursion
let i = parser.stack.length; let i = parser.stack.length;
let legal = false; let legal = false;
while (i--) { while (i--) {
const fragment = parser.stack[i]; const fragment = parser.stack[i];
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') { if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock' || fragment.type === 'InlineComponent') {
legal = true; legal = true;
break; break;
} }
} }
if (!legal) { if (!legal) {
parser.error({ parser.error(
code: 'invalid-self-placement', {
message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components' code: 'invalid-self-placement',
}, start); message: '<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, or slots passed to components',
} },
start
);
}
return 'svelte:self'; return 'svelte:self';
} }
if (parser.read(COMPONENT)) return 'svelte:component'; if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(SLOT)) return 'svelte:fragment'; if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(/(\s|\/|>)/); const name = parser.read_until(/(\s|\/|>)/);
if (meta_tags.has(name)) return name; if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) { if (name.startsWith('svelte:')) {
const match = fuzzymatch(name.slice(7), valid_meta_tags); const match = fuzzymatch(name.slice(7), valid_meta_tags);
let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`; let message = `Valid <svelte:...> tag names are ${list(valid_meta_tags)}`;
if (match) message += ` (did you mean '${match}'?)`; if (match) message += ` (did you mean '${match}'?)`;
parser.error({ parser.error(
code: 'invalid-tag-name', {
message code: 'invalid-tag-name',
}, start); message,
} },
start
);
}
if (!valid_tag_name.test(name)) { if (!valid_tag_name.test(name)) {
parser.error({ parser.error(
code: 'invalid-tag-name', {
message: 'Expected valid tag name' code: 'invalid-tag-name',
}, start); message: 'Expected valid tag name',
} },
start
);
}
return name; return name;
} }
function read_attribute(parser: Parser, unique_names: Set<string>) { function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index; const start = parser.index;
function check_unique(name: string) { function check_unique(name: string) {
if (unique_names.has(name)) { if (unique_names.has(name)) {
parser.error({ parser.error(
code: 'duplicate-attribute', {
message: 'Attributes need to be unique' code: 'duplicate-attribute',
}, start); message: 'Attributes need to be unique',
} },
unique_names.add(name); start
} );
}
unique_names.add(name);
}
if (parser.eat('{')) { if (parser.eat('{')) {
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.eat('...')) { if (parser.eat('...')) {
const expression = read_expression(parser); const expression = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
return { return {
start, start,
end: parser.index, end: parser.index,
type: 'Spread', type: 'Spread',
expression expression,
}; };
} else { } else {
const value_start = parser.index; const value_start = parser.index;
const name = parser.read_identifier(); const name = parser.read_identifier();
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
check_unique(name); check_unique(name);
return { return {
start, start,
end: parser.index, end: parser.index,
type: 'Attribute', type: 'Attribute',
name, name,
value: [{ value: [
start: value_start, {
end: value_start + name.length, start: value_start,
type: 'AttributeShorthand', end: value_start + name.length,
expression: { type: 'AttributeShorthand',
start: value_start, expression: {
end: value_start + name.length, start: value_start,
type: 'Identifier', end: value_start + name.length,
name type: 'Identifier',
} name,
}] },
}; },
} ],
} };
}
}
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const name = parser.read_until(/[\s=\/>"']/); const name = parser.read_until(/[\s=\/>"']/);
if (!name) return null; if (!name) return null;
let end = parser.index; let end = parser.index;
parser.allow_whitespace(); parser.allow_whitespace();
const colon_index = name.indexOf(':'); const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index)); const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
let value: any[] | true = true; let value: any[] | true = true;
if (parser.eat('=')) { if (parser.eat('=')) {
parser.allow_whitespace(); parser.allow_whitespace();
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
} else if (parser.match_regex(/["']/)) { } else if (parser.match_regex(/["']/)) {
parser.error({ parser.error(
code: 'unexpected-token', {
message: 'Expected =' code: 'unexpected-token',
}, parser.index); message: 'Expected =',
} },
parser.index
);
}
if (type) { if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|'); const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (type === 'Binding' && directive_name !== 'this') { if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name); check_unique(directive_name);
} else if (type !== 'EventHandler' && type !== 'Action') { } else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name); check_unique(name);
} }
if (type === 'Ref') { if (type === 'Ref') {
parser.error({ parser.error(
code: 'invalid-ref-directive', {
message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead` code: 'invalid-ref-directive',
}, start); message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`,
} },
start
);
}
if (type === 'Class' && directive_name === '') { if (type === 'Class' && directive_name === '') {
parser.error({ parser.error(
code: 'invalid-class-directive', {
message: 'Class binding name cannot be empty' code: 'invalid-class-directive',
}, start + colon_index + 1); message: 'Class binding name cannot be empty',
} },
start + colon_index + 1
);
}
if (value[0]) { if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') { if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error({ parser.error(
code: 'invalid-directive-value', {
message: 'Directive value must be a JavaScript expression enclosed in curly braces' code: 'invalid-directive-value',
}, value[0].start); message: 'Directive value must be a JavaScript expression enclosed in curly braces',
} },
} value[0].start
);
}
}
const directive: Directive = { const directive: Directive = {
start, start,
end, end,
type, type,
name: directive_name, name: directive_name,
modifiers, modifiers,
expression: (value[0] && value[0].expression) || null expression: (value[0] && value[0].expression) || null,
}; };
if (type === 'Transition') { if (type === 'Transition') {
const direction = name.slice(0, colon_index); const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition'; directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition'; directive.outro = direction === 'out' || direction === 'transition';
} }
if (!directive.expression && (type === 'Binding' || type === 'Class')) { if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = { directive.expression = {
start: directive.start + colon_index + 1, start: directive.start + colon_index + 1,
end: directive.end, end: directive.end,
type: 'Identifier', type: 'Identifier',
name: directive.name name: directive.name,
} as any; } as any;
} }
return directive; return directive;
} }
check_unique(name); check_unique(name);
return { return {
start, start,
end, end,
type: 'Attribute', type: 'Attribute',
name, name,
value value,
}; };
} }
function get_directive_type(name: string): DirectiveType { function get_directive_type(name: string): DirectiveType {
if (name === 'use') return 'Action'; if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation'; if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding'; if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class'; if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler'; if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let'; if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref'; if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition'; if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
} }
function read_attribute_value(parser: Parser) { function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null; const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
const regex = ( const regex = quote_mark === "'" ? /'/ : quote_mark === '"' ? /"/ : /(\/>|[\s"'=<>`])/;
quote_mark === "'" ? /'/ :
quote_mark === '"' ? /"/ :
/(\/>|[\s"'=<>`])/
);
const value = read_sequence(parser, () => !!parser.match_regex(regex)); const value = read_sequence(parser, () => !!parser.match_regex(regex));
if (quote_mark) parser.index += 1; if (quote_mark) parser.index += 1;
return value; return value;
} }
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] { function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
let current_chunk: Text = { let current_chunk: Text = {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'Text', type: 'Text',
raw: '', raw: '',
data: null data: null,
}; };
function flush() { function flush() {
if (current_chunk.raw) { if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw); current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index; current_chunk.end = parser.index;
chunks.push(current_chunk); chunks.push(current_chunk);
} }
} }
const chunks: TemplateNode[] = []; const chunks: TemplateNode[] = [];
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
const index = parser.index; const index = parser.index;
if (done()) { if (done()) {
flush(); flush();
return chunks; return chunks;
} else if (parser.eat('{')) { } else if (parser.eat('{')) {
flush(); flush();
parser.allow_whitespace(); parser.allow_whitespace();
const expression = read_expression(parser); const expression = read_expression(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
chunks.push({ chunks.push({
start: index, start: index,
end: parser.index, end: parser.index,
type: 'MustacheTag', type: 'MustacheTag',
expression expression,
}); });
current_chunk = { current_chunk = {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'Text', type: 'Text',
raw: '', raw: '',
data: null data: null,
}; };
} else { } else {
current_chunk.raw += parser.template[parser.index++]; current_chunk.raw += parser.template[parser.index++];
} }
} }
parser.error({ parser.error({
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input',
}); });
} }

View file

@ -4,25 +4,21 @@ import { decode_character_references } from '../utils/html.js';
import { Parser } from '../index.js'; import { Parser } from '../index.js';
export default function text(parser: Parser) { export default function text(parser: Parser) {
const start = parser.index; const start = parser.index;
let data = ''; let data = '';
while ( while (parser.index < parser.template.length && !parser.match('<') && !parser.match('{')) {
parser.index < parser.template.length && data += parser.template[parser.index++];
!parser.match('<') && }
!parser.match('{')
) {
data += parser.template[parser.index++];
}
const node = { const node = {
start, start,
end: parser.index, end: parser.index,
type: 'Text', type: 'Text',
raw: data, raw: data,
data: decode_character_references(data) data: decode_character_references(data),
}; };
parser.current().children.push(node); parser.current().children.push(node);
} }

View file

@ -6,25 +6,22 @@ const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0); const CURLY_BRACKET_CLOSE = '}'.charCodeAt(0);
export function is_bracket_open(code) { export function is_bracket_open(code) {
return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN; return code === SQUARE_BRACKET_OPEN || code === CURLY_BRACKET_OPEN;
} }
export function is_bracket_close(code) { export function is_bracket_close(code) {
return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE; return code === SQUARE_BRACKET_CLOSE || code === CURLY_BRACKET_CLOSE;
} }
export function is_bracket_pair(open, close) { export function is_bracket_pair(open, close) {
return ( return (open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) || (open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE);
(open === SQUARE_BRACKET_OPEN && close === SQUARE_BRACKET_CLOSE) ||
(open === CURLY_BRACKET_OPEN && close === CURLY_BRACKET_CLOSE)
);
} }
export function get_bracket_close(open) { export function get_bracket_close(open) {
if (open === SQUARE_BRACKET_OPEN) { if (open === SQUARE_BRACKET_OPEN) {
return SQUARE_BRACKET_CLOSE; return SQUARE_BRACKET_CLOSE;
} }
if (open === CURLY_BRACKET_OPEN) { if (open === CURLY_BRACKET_OPEN) {
return CURLY_BRACKET_CLOSE; return CURLY_BRACKET_CLOSE;
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -3,64 +3,61 @@
import entities from './entities.js'; import entities from './entities.js';
const windows_1252 = [ const windows_1252 = [
8364, 8364,
129, 129,
8218, 8218,
402, 402,
8222, 8222,
8230, 8230,
8224, 8224,
8225, 8225,
710, 710,
8240, 8240,
352, 352,
8249, 8249,
338, 338,
141, 141,
381, 381,
143, 143,
144, 144,
8216, 8216,
8217, 8217,
8220, 8220,
8221, 8221,
8226, 8226,
8211, 8211,
8212, 8212,
732, 732,
8482, 8482,
353, 353,
8250, 8250,
339, 339,
157, 157,
382, 382,
376 376,
]; ];
const entity_pattern = new RegExp( const entity_pattern = new RegExp(`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`, 'g');
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(entities).join('|')}))(?:;|\\b)`,
'g'
);
export function decode_character_references(html: string) { export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => { return html.replace(entity_pattern, (match, entity) => {
let code; let code;
// Handle named entities // Handle named entities
if (entity[0] !== '#') { if (entity[0] !== '#') {
code = entities[entity]; code = entities[entity];
} else if (entity[1] === 'x') { } else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16); code = parseInt(entity.substring(2), 16);
} else { } else {
code = parseInt(entity.substring(1), 10); code = parseInt(entity.substring(1), 10);
} }
if (!code) { if (!code) {
return match; return match;
} }
return String.fromCodePoint(validate_code(code)); return String.fromCodePoint(validate_code(code));
}); });
} }
const NUL = 0; const NUL = 0;
@ -71,83 +68,76 @@ const NUL = 0;
// //
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
function validate_code(code: number) { function validate_code(code: number) {
// line feed becomes generic whitespace // line feed becomes generic whitespace
if (code === 10) { if (code === 10) {
return 32; return 32;
} }
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...) // ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if (code < 128) { if (code < 128) {
return code; return code;
} }
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need // code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on // to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) { if (code <= 159) {
return windows_1252[code - 128]; return windows_1252[code - 128];
} }
// basic multilingual plane // basic multilingual plane
if (code < 55296) { if (code < 55296) {
return code; return code;
} }
// UTF-16 surrogate halves // UTF-16 surrogate halves
if (code <= 57343) { if (code <= 57343) {
return NUL; return NUL;
} }
// rest of the basic multilingual plane // rest of the basic multilingual plane
if (code <= 65535) { if (code <= 65535) {
return code; return code;
} }
// supplementary multilingual plane 0x10000 - 0x1ffff // supplementary multilingual plane 0x10000 - 0x1ffff
if (code >= 65536 && code <= 131071) { if (code >= 65536 && code <= 131071) {
return code; return code;
} }
// supplementary ideographic plane 0x20000 - 0x2ffff // supplementary ideographic plane 0x20000 - 0x2ffff
if (code >= 131072 && code <= 196607) { if (code >= 131072 && code <= 196607) {
return code; return code;
} }
return NUL; return NUL;
} }
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const disallowed_contents = new Map([ const disallowed_contents = new Map([
['li', new Set(['li'])], ['li', new Set(['li'])],
['dt', new Set(['dt', 'dd'])], ['dt', new Set(['dt', 'dd'])],
['dd', new Set(['dt', 'dd'])], ['dd', new Set(['dt', 'dd'])],
[ ['p', new Set('address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(' '))],
'p', ['rt', new Set(['rt', 'rp'])],
new Set( ['rp', new Set(['rt', 'rp'])],
'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split( ['optgroup', new Set(['optgroup'])],
' ' ['option', new Set(['option', 'optgroup'])],
) ['thead', new Set(['tbody', 'tfoot'])],
) ['tbody', new Set(['tbody', 'tfoot'])],
], ['tfoot', new Set(['tbody'])],
['rt', new Set(['rt', 'rp'])], ['tr', new Set(['tr', 'tbody'])],
['rp', new Set(['rt', 'rp'])], ['td', new Set(['td', 'th', 'tr'])],
['optgroup', new Set(['optgroup'])], ['th', new Set(['td', 'th', 'tr'])],
['option', new Set(['option', 'optgroup'])],
['thead', new Set(['tbody', 'tfoot'])],
['tbody', new Set(['tbody', 'tfoot'])],
['tfoot', new Set(['tbody'])],
['tr', new Set(['tr', 'tbody'])],
['td', new Set(['td', 'th', 'tr'])],
['th', new Set(['td', 'th', 'tr'])]
]); ]);
// can this be a child of the parent element, or does it implicitly // can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`? // close it, like `<li>one<li>two`?
export function closing_tag_omitted(current: string, next?: string) { export function closing_tag_omitted(current: string, next?: string) {
if (disallowed_contents.has(current)) { if (disallowed_contents.has(current)) {
if (!next || disallowed_contents.get(current).has(next)) { if (!next || disallowed_contents.get(current).has(next)) {
return true; return true;
} }
} }
return false; return false;
} }

View file

@ -1,30 +1,30 @@
import { TemplateNode } from '../../interfaces.js'; import { TemplateNode } from '../../interfaces.js';
export function to_string(node: TemplateNode) { export function to_string(node: TemplateNode) {
switch (node.type) { switch (node.type) {
case 'IfBlock': case 'IfBlock':
return '{#if} block'; return '{#if} block';
case 'ThenBlock': case 'ThenBlock':
return '{:then} block'; return '{:then} block';
case 'ElseBlock': case 'ElseBlock':
return '{:else} block'; return '{:else} block';
case 'PendingBlock': case 'PendingBlock':
case 'AwaitBlock': case 'AwaitBlock':
return '{#await} block'; return '{#await} block';
case 'CatchBlock': case 'CatchBlock':
return '{:catch} block'; return '{:catch} block';
case 'EachBlock': case 'EachBlock':
return '{#each} block'; return '{#each} block';
case 'RawMustacheTag': case 'RawMustacheTag':
return '{@html} block'; return '{@html} block';
case 'DebugTag': case 'DebugTag':
return '{@debug} block'; return '{@debug} block';
case 'Element': case 'Element':
case 'InlineComponent': case 'InlineComponent':
case 'Slot': case 'Slot':
case 'Title': case 'Title':
return `<${node.name}> tag`; return `<${node.name}> tag`;
default: default:
return node.type; return node.type;
} }
} }

View file

@ -4,39 +4,42 @@ import { locate } from 'locate-character';
import get_code_frame from './get_code_frame.js'; import get_code_frame from './get_code_frame.js';
export class CompileError extends Error { export class CompileError extends Error {
code: string; code: string;
start: { line: number; column: number }; start: { line: number; column: number };
end: { line: number; column: number }; end: { line: number; column: number };
pos: number; pos: number;
filename: string; filename: string;
frame: string; frame: string;
toString() { toString() {
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`; return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
} }
} }
export default function error(message: string, props: { export default function error(
name: string; message: string,
code: string; props: {
source: string; name: string;
filename: string; code: string;
start: number; source: string;
end?: number; filename: string;
}): never { start: number;
const error = new CompileError(message); end?: number;
error.name = props.name; }
): never {
const error = new CompileError(message);
error.name = props.name;
const start = locate(props.source, props.start, { offsetLine: 1 }); const start = locate(props.source, props.start, { offsetLine: 1 });
const end = locate(props.source, props.end || props.start, { offsetLine: 1 }); const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
error.code = props.code; error.code = props.code;
error.start = start; error.start = start;
error.end = end; error.end = end;
error.pos = props.start; error.pos = props.start;
error.filename = props.filename; error.filename = props.filename;
error.frame = get_code_frame(props.source, start.line - 1, start.column); error.frame = get_code_frame(props.source, start.line - 1, start.column);
throw error; throw error;
} }

View file

@ -2,9 +2,9 @@
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE // Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
export default function full_char_code_at(str: string, i: number): number { export default function full_char_code_at(str: string, i: number): number {
const code = str.charCodeAt(i); const code = str.charCodeAt(i);
if (code <= 0xd7ff || code >= 0xe000) return code; if (code <= 0xd7ff || code >= 0xe000) return code;
const next = str.charCodeAt(i + 1); const next = str.charCodeAt(i + 1);
return (code << 10) + next - 0x35fdc00; return (code << 10) + next - 0x35fdc00;
} }

View file

@ -1,10 +1,10 @@
// @ts-nocheck // @ts-nocheck
export default function fuzzymatch(name: string, names: string[]) { export default function fuzzymatch(name: string, names: string[]) {
const set = new FuzzySet(names); const set = new FuzzySet(names);
const matches = set.get(name); const matches = set.get(name);
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null; return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
} }
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js // adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
@ -15,225 +15,215 @@ const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1 // return an edit distance from 0 to 1
function _distance(str1: string, str2: string) { function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null) { if (str1 === null && str2 === null) {
throw 'Trying to compare two null values'; throw 'Trying to compare two null values';
} }
if (str1 === null || str2 === null) return 0; if (str1 === null || str2 === null) return 0;
str1 = String(str1); str1 = String(str1);
str2 = String(str2); str2 = String(str2);
const distance = levenshtein(str1, str2); const distance = levenshtein(str1, str2);
if (str1.length > str2.length) { if (str1.length > str2.length) {
return 1 - distance / str1.length; return 1 - distance / str1.length;
} else { } else {
return 1 - distance / str2.length; return 1 - distance / str2.length;
} }
} }
// helper functions // helper functions
function levenshtein(str1: string, str2: string) { function levenshtein(str1: string, str2: string) {
const current: number[] = []; const current: number[] = [];
let prev; let prev;
let value; let value;
for (let i = 0; i <= str2.length; i++) { for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) { for (let j = 0; j <= str1.length; j++) {
if (i && j) { if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) { if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev; value = prev;
} else { } else {
value = Math.min(current[j], current[j - 1], prev) + 1; value = Math.min(current[j], current[j - 1], prev) + 1;
} }
} else { } else {
value = i + j; value = i + j;
} }
prev = current[j]; prev = current[j];
current[j] = value; current[j] = value;
} }
} }
return current.pop(); return current.pop();
} }
const non_word_regex = /[^\w, ]+/; const non_word_regex = /[^\w, ]+/;
function iterate_grams(value: string, gram_size = 2) { function iterate_grams(value: string, gram_size = 2) {
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-'; const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
const len_diff = gram_size - simplified.length; const len_diff = gram_size - simplified.length;
const results = []; const results = [];
if (len_diff > 0) { if (len_diff > 0) {
for (let i = 0; i < len_diff; ++i) { for (let i = 0; i < len_diff; ++i) {
value += '-'; value += '-';
} }
} }
for (let i = 0; i < simplified.length - gram_size + 1; ++i) { for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
results.push(simplified.slice(i, i + gram_size)); results.push(simplified.slice(i, i + gram_size));
} }
return results; return results;
} }
function gram_counter(value: string, gram_size = 2) { function gram_counter(value: string, gram_size = 2) {
// return an object where key=gram, value=number of occurrences // return an object where key=gram, value=number of occurrences
const result = {}; const result = {};
const grams = iterate_grams(value, gram_size); const grams = iterate_grams(value, gram_size);
let i = 0; let i = 0;
for (i; i < grams.length; ++i) { for (i; i < grams.length; ++i) {
if (grams[i] in result) { if (grams[i] in result) {
result[grams[i]] += 1; result[grams[i]] += 1;
} else { } else {
result[grams[i]] = 1; result[grams[i]] = 1;
} }
} }
return result; return result;
} }
function sort_descending(a, b) { function sort_descending(a, b) {
return b[0] - a[0]; return b[0] - a[0];
} }
class FuzzySet { class FuzzySet {
exact_set = {}; exact_set = {};
match_dict = {}; match_dict = {};
items = {}; items = {};
constructor(arr: string[]) { constructor(arr: string[]) {
// initialization // initialization
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) { for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = []; this.items[i] = [];
} }
// add all the items to the set // add all the items to the set
for (let i = 0; i < arr.length; ++i) { for (let i = 0; i < arr.length; ++i) {
this.add(arr[i]); this.add(arr[i]);
} }
} }
add(value: string) { add(value: string) {
const normalized_value = value.toLowerCase(); const normalized_value = value.toLowerCase();
if (normalized_value in this.exact_set) { if (normalized_value in this.exact_set) {
return false; return false;
} }
let i = GRAM_SIZE_LOWER; let i = GRAM_SIZE_LOWER;
for (i; i < GRAM_SIZE_UPPER + 1; ++i) { for (i; i < GRAM_SIZE_UPPER + 1; ++i) {
this._add(value, i); this._add(value, i);
} }
} }
_add(value: string, gram_size: number) { _add(value: string, gram_size: number) {
const normalized_value = value.toLowerCase(); const normalized_value = value.toLowerCase();
const items = this.items[gram_size] || []; const items = this.items[gram_size] || [];
const index = items.length; const index = items.length;
items.push(0); items.push(0);
const gram_counts = gram_counter(normalized_value, gram_size); const gram_counts = gram_counter(normalized_value, gram_size);
let sum_of_square_gram_counts = 0; let sum_of_square_gram_counts = 0;
let gram; let gram;
let gram_count; let gram_count;
for (gram in gram_counts) { for (gram in gram_counts) {
gram_count = gram_counts[gram]; gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2); sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) { if (gram in this.match_dict) {
this.match_dict[gram].push([index, gram_count]); this.match_dict[gram].push([index, gram_count]);
} else { } else {
this.match_dict[gram] = [[index, gram_count]]; this.match_dict[gram] = [[index, gram_count]];
} }
} }
const vector_normal = Math.sqrt(sum_of_square_gram_counts); const vector_normal = Math.sqrt(sum_of_square_gram_counts);
items[index] = [vector_normal, normalized_value]; items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items; this.items[gram_size] = items;
this.exact_set[normalized_value] = value; this.exact_set[normalized_value] = value;
} }
get(value: string) { get(value: string) {
const normalized_value = value.toLowerCase(); const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value]; const result = this.exact_set[normalized_value];
if (result) { if (result) {
return [[1, result]]; return [[1, result]];
} }
let results = []; let results = [];
// start with high gram size and if there are no results, go to lower gram sizes // start with high gram size and if there are no results, go to lower gram sizes
for ( for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) {
let gram_size = GRAM_SIZE_UPPER; results = this.__get(value, gram_size);
gram_size >= GRAM_SIZE_LOWER; if (results) {
--gram_size return results;
) { }
results = this.__get(value, gram_size); }
if (results) { return null;
return results; }
}
}
return null;
}
__get(value: string, gram_size: number) { __get(value: string, gram_size: number) {
const normalized_value = value.toLowerCase(); const normalized_value = value.toLowerCase();
const matches = {}; const matches = {};
const gram_counts = gram_counter(normalized_value, gram_size); const gram_counts = gram_counter(normalized_value, gram_size);
const items = this.items[gram_size]; const items = this.items[gram_size];
let sum_of_square_gram_counts = 0; let sum_of_square_gram_counts = 0;
let gram; let gram;
let gram_count; let gram_count;
let i; let i;
let index; let index;
let other_gram_count; let other_gram_count;
for (gram in gram_counts) { for (gram in gram_counts) {
gram_count = gram_counts[gram]; gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2); sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) { if (gram in this.match_dict) {
for (i = 0; i < this.match_dict[gram].length; ++i) { for (i = 0; i < this.match_dict[gram].length; ++i) {
index = this.match_dict[gram][i][0]; index = this.match_dict[gram][i][0];
other_gram_count = this.match_dict[gram][i][1]; other_gram_count = this.match_dict[gram][i][1];
if (index in matches) { if (index in matches) {
matches[index] += gram_count * other_gram_count; matches[index] += gram_count * other_gram_count;
} else { } else {
matches[index] = gram_count * other_gram_count; matches[index] = gram_count * other_gram_count;
} }
} }
} }
} }
const vector_normal = Math.sqrt(sum_of_square_gram_counts); const vector_normal = Math.sqrt(sum_of_square_gram_counts);
let results = []; let results = [];
let match_score; let match_score;
// build a results list of [score, str] // build a results list of [score, str]
for (const match_index in matches) { for (const match_index in matches) {
match_score = matches[match_index]; match_score = matches[match_index];
results.push([ results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]);
match_score / (vector_normal * items[match_index][0]), }
items[match_index][1]
]);
}
results.sort(sort_descending); results.sort(sort_descending);
let new_results = []; let new_results = [];
const end_index = Math.min(50, results.length); const end_index = Math.min(50, results.length);
// truncate somewhat arbitrarily to 50 // truncate somewhat arbitrarily to 50
for (let i = 0; i < end_index; ++i) { for (let i = 0; i < end_index; ++i) {
new_results.push([ new_results.push([_distance(results[i][1], normalized_value), results[i][1]]);
_distance(results[i][1], normalized_value), }
results[i][1] results = new_results;
]); results.sort(sort_descending);
}
results = new_results;
results.sort(sort_descending);
new_results = []; new_results = [];
for (let i = 0; i < results.length; ++i) { for (let i = 0; i < results.length; ++i) {
if (results[i][0] == results[0][0]) { if (results[i][0] == results[0][0]) {
new_results.push([results[i][0], this.exact_set[results[i][1]]]); new_results.push([results[i][0], this.exact_set[results[i][1]]]);
} }
} }
return new_results; return new_results;
} }
} }

View file

@ -1,31 +1,27 @@
function tabs_to_spaces(str: string) { function tabs_to_spaces(str: string) {
return str.replace(/^\t+/, match => match.split('\t').join(' ')); return str.replace(/^\t+/, (match) => match.split('\t').join(' '));
} }
export default function get_code_frame( export default function get_code_frame(source: string, line: number, column: number) {
source: string, const lines = source.split('\n');
line: number,
column: number
) {
const lines = source.split('\n');
const frame_start = Math.max(0, line - 2); const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length); const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length; const digits = String(frame_end + 1).length;
return lines return lines
.slice(frame_start, frame_end) .slice(frame_start, frame_end)
.map((str, i) => { .map((str, i) => {
const isErrorLine = frame_start + i === line; const isErrorLine = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' '); const line_num = String(i + frame_start + 1).padStart(digits, ' ');
if (isErrorLine) { if (isErrorLine) {
const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^'; const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`; return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
} }
return `${line_num}: ${tabs_to_spaces(str)}`; return `${line_num}: ${tabs_to_spaces(str)}`;
}) })
.join('\n'); .join('\n');
} }

View file

@ -1,4 +1,4 @@
export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) { export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) {
prev.next = next; prev.next = next;
if (next) next.prev = prev; if (next) next.prev = prev;
} }

View file

@ -1,6 +1,4 @@
export default function list(items: string[], conjunction = 'or') { export default function list(items: string[], conjunction = 'or') {
if (items.length === 1) return items[0]; if (items.length === 1) return items[0];
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[ return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
items.length - 1
]}`;
} }

View file

@ -2,138 +2,138 @@ import { isIdentifierStart, isIdentifierChar } from 'acorn';
import full_char_code_at from './full_char_code_at.js'; import full_char_code_at from './full_char_code_at.js';
export const globals = new Set([ export const globals = new Set([
'alert', 'alert',
'Array', 'Array',
'Boolean', 'Boolean',
'clearInterval', 'clearInterval',
'clearTimeout', 'clearTimeout',
'confirm', 'confirm',
'console', 'console',
'Date', 'Date',
'decodeURI', 'decodeURI',
'decodeURIComponent', 'decodeURIComponent',
'document', 'document',
'Element', 'Element',
'encodeURI', 'encodeURI',
'encodeURIComponent', 'encodeURIComponent',
'Error', 'Error',
'EvalError', 'EvalError',
'Event', 'Event',
'EventSource', 'EventSource',
'fetch', 'fetch',
'global', 'global',
'globalThis', 'globalThis',
'history', 'history',
'Infinity', 'Infinity',
'InternalError', 'InternalError',
'Intl', 'Intl',
'isFinite', 'isFinite',
'isNaN', 'isNaN',
'JSON', 'JSON',
'localStorage', 'localStorage',
'location', 'location',
'Map', 'Map',
'Math', 'Math',
'NaN', 'NaN',
'navigator', 'navigator',
'Number', 'Number',
'Node', 'Node',
'Object', 'Object',
'parseFloat', 'parseFloat',
'parseInt', 'parseInt',
'process', 'process',
'Promise', 'Promise',
'prompt', 'prompt',
'RangeError', 'RangeError',
'ReferenceError', 'ReferenceError',
'RegExp', 'RegExp',
'sessionStorage', 'sessionStorage',
'Set', 'Set',
'setInterval', 'setInterval',
'setTimeout', 'setTimeout',
'String', 'String',
'SyntaxError', 'SyntaxError',
'TypeError', 'TypeError',
'undefined', 'undefined',
'URIError', 'URIError',
'URL', 'URL',
'window' 'window',
]); ]);
export const reserved = new Set([ export const reserved = new Set([
'arguments', 'arguments',
'await', 'await',
'break', 'break',
'case', 'case',
'catch', 'catch',
'class', 'class',
'const', 'const',
'continue', 'continue',
'debugger', 'debugger',
'default', 'default',
'delete', 'delete',
'do', 'do',
'else', 'else',
'enum', 'enum',
'eval', 'eval',
'export', 'export',
'extends', 'extends',
'false', 'false',
'finally', 'finally',
'for', 'for',
'function', 'function',
'if', 'if',
'implements', 'implements',
'import', 'import',
'in', 'in',
'instanceof', 'instanceof',
'interface', 'interface',
'let', 'let',
'new', 'new',
'null', 'null',
'package', 'package',
'private', 'private',
'protected', 'protected',
'public', 'public',
'return', 'return',
'static', 'static',
'super', 'super',
'switch', 'switch',
'this', 'this',
'throw', 'throw',
'true', 'true',
'try', 'try',
'typeof', 'typeof',
'var', 'var',
'void', 'void',
'while', 'while',
'with', 'with',
'yield' 'yield',
]); ]);
const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
export function is_void(name: string) { export function is_void(name: string) {
return void_element_names.test(name) || name.toLowerCase() === '!doctype'; return void_element_names.test(name) || name.toLowerCase() === '!doctype';
} }
export function is_valid(str: string): boolean { export function is_valid(str: string): boolean {
let i = 0; let i = 0;
while (i < str.length) { while (i < str.length) {
const code = full_char_code_at(str, i); const code = full_char_code_at(str, i);
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false; if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
i += code <= 0xffff ? 1 : 2; i += code <= 0xffff ? 1 : 2;
} }
return true; return true;
} }
export function sanitize(name: string) { export function sanitize(name: string) {
return name return name
.replace(/[^a-zA-Z0-9_]+/g, '_') .replace(/[^a-zA-Z0-9_]+/g, '_')
.replace(/^_/, '') .replace(/^_/, '')
.replace(/_$/, '') .replace(/_$/, '')
.replace(/^[0-9]/, '_$&'); .replace(/^[0-9]/, '_$&');
} }

View file

@ -8,21 +8,6 @@ export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace'; export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns'; export const xmlns = 'http://www.w3.org/2000/xmlns';
export const valid_namespaces = [ export const valid_namespaces = ['foreign', 'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', foreign, html, mathml, svg, xlink, xml, xmlns];
'foreign',
'html',
'mathml',
'svg',
'xlink',
'xml',
'xmlns',
foreign,
html,
mathml,
svg,
xlink,
xml,
xmlns
];
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns }; export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };

View file

@ -1,34 +1,34 @@
// @ts-nocheck // @ts-nocheck
export function nodes_match(a, b) { export function nodes_match(a, b) {
if (!!a !== !!b) return false; if (!!a !== !!b) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false; if (Array.isArray(a) !== Array.isArray(b)) return false;
if (a && typeof a === 'object') { if (a && typeof a === 'object') {
if (Array.isArray(a)) { if (Array.isArray(a)) {
if (a.length !== b.length) return false; if (a.length !== b.length) return false;
return a.every((child, i) => nodes_match(child, b[i])); return a.every((child, i) => nodes_match(child, b[i]));
} }
const a_keys = Object.keys(a).sort(); const a_keys = Object.keys(a).sort();
const b_keys = Object.keys(b).sort(); const b_keys = Object.keys(b).sort();
if (a_keys.length !== b_keys.length) return false; if (a_keys.length !== b_keys.length) return false;
let i = a_keys.length; let i = a_keys.length;
while (i--) { while (i--) {
const key = a_keys[i]; const key = a_keys[i];
if (b_keys[i] !== key) return false; if (b_keys[i] !== key) return false;
if (key === 'start' || key === 'end') continue; if (key === 'start' || key === 'end') continue;
if (!nodes_match(a[key], b[key])) { if (!nodes_match(a[key], b[key])) {
return false; return false;
} }
} }
return true; return true;
} }
return a === b; return a === b;
} }

View file

@ -1,15 +1,15 @@
import { whitespace } from './patterns.js'; import { whitespace } from './patterns.js';
export function trim_start(str: string) { export function trim_start(str: string) {
let i = 0; let i = 0;
while (whitespace.test(str[i])) i += 1; while (whitespace.test(str[i])) i += 1;
return str.slice(i); return str.slice(i);
} }
export function trim_end(str: string) { export function trim_end(str: string) {
let i = str.length; let i = str.length;
while (whitespace.test(str[i - 1])) i -= 1; while (whitespace.test(str[i - 1])) i -= 1;
return str.slice(0, i); return str.slice(0, i);
} }

22
src/config.ts Normal file
View 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;
}

View file

@ -1,12 +1,11 @@
import type { AstroConfig } from './@types/astro'; import type { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger.js'; import type { LogOptions } from './logger.js';
import { loadConfiguration, startServer as startSnowpackServer, logger as snowpackLogger } from 'snowpack';
import { existsSync, promises as fsPromises } from 'fs'; import { logger as snowpackLogger } from 'snowpack';
import http from 'http'; import http from 'http';
import { relative as pathRelative } from 'path'; import { relative as pathRelative } from 'path';
import { defaultLogDestination, info, error, parseError } from './logger.js'; import { defaultLogDestination, error, parseError } from './logger.js';
import { createRuntime } from './runtime.js';
const { readFile } = fsPromises;
const hostname = '127.0.0.1'; const hostname = '127.0.0.1';
const port = 3000; const port = 3000;
@ -20,95 +19,45 @@ const logging: LogOptions = {
}; };
export default async function (astroConfig: AstroConfig) { export default async function (astroConfig: AstroConfig) {
const { projectRoot, hmxRoot } = astroConfig; const { projectRoot } = astroConfig;
const internalPath = new URL('./frontend/', import.meta.url); const runtime = await createRuntime(astroConfig, logging);
const snowpackConfigPath = new URL('./snowpack.config.js', projectRoot);
// Workaround for SKY-251
const hmxPlugOptions: { resolve?: (s: string) => string } = {};
if (existsSync(new URL('./package-lock.json', projectRoot))) {
const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
const pkgLock = JSON.parse(pkgLockStr);
hmxPlugOptions.resolve = (pkgName: string) => {
const ver = pkgLock.dependencies[pkgName].version;
return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
};
}
const snowpackConfig = await loadConfiguration(
{
root: projectRoot.pathname,
mount: {
[hmxRoot.pathname]: '/_hmx',
[internalPath.pathname]: '/__hmx_internal__',
},
plugins: [['astro/snowpack-plugin', hmxPlugOptions]],
devOptions: {
open: 'none',
output: 'stream',
port: 0,
},
packageOptions: {
knownEntrypoints: ['preact-render-to-string'],
external: ['@vue/server-renderer'],
},
},
snowpackConfigPath.pathname
);
const snowpack = await startSnowpackServer({
config: snowpackConfig,
lockfile: null,
});
const runtime = snowpack.getServerRuntime();
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
const fullurl = new URL(req.url || '/', 'https://example.org/'); const result = await runtime.load(req.url);
const reqPath = decodeURI(fullurl.pathname);
const selectedPage = reqPath.substr(1) || 'index';
info(logging, 'access', reqPath);
const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot); switch (result.statusCode) {
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot); case 200: {
const selectedPageUrl = `/_hmx/pages/${selectedPage}.js`;
// Non-hmx pages
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
try {
const result = await snowpack.loadUrl(reqPath);
if (result.contentType) { if (result.contentType) {
res.setHeader('Content-Type', result.contentType); res.setHeader('Content-Type', result.contentType);
} }
res.write(result.contents); res.write(result.contents);
res.end(); res.end();
} catch (err) { break;
}
case 404: {
const fullurl = new URL(req.url || '/', 'https://example.org/');
const reqPath = decodeURI(fullurl.pathname);
error(logging, 'static', 'Not found', reqPath); error(logging, 'static', 'Not found', reqPath);
res.statusCode = 404; res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Type', 'text/plain');
res.end('Not Found'); res.end('Not Found');
break;
} }
return; case 500: {
} switch (result.type) {
case 'parse-error': {
try { const err = result.error;
const mod = await runtime.importModule(selectedPageUrl); err.filename = pathRelative(projectRoot.pathname, err.filename);
const html = await mod.exports.default(); parseError(logging, err);
res.statusCode = 200; break;
res.setHeader('Content-Type', 'text/html; charset=utf-8'); }
res.end(html); default: {
} catch (err) { error(logging, 'executing hmx', result.error);
switch (err.code) { break;
case 'parse-error': { }
err.filename = pathRelative(projectRoot.pathname, err.filename);
debugger;
parseError(logging, err);
break;
}
default: {
console.error(err.code, err);
error(logging, 'running hmx', err);
break;
} }
break;
} }
} }
}); });

View file

@ -1,31 +1,32 @@
import type { CompileError } from './compiler/utils/error.js'; import type { CompileError } from './compiler/utils/error.js';
import { bold, blue, red, grey, underline } from 'kleur/colors'; import { bold, blue, red, grey, underline } from 'kleur/colors';
import { Writable } from 'stream'; import { Writable } from 'stream';
import { format as utilFormat } from 'util';
type ConsoleStream = Writable & { type ConsoleStream = Writable & {
fd: 1 | 2 fd: 1 | 2;
}; };
export const defaultLogDestination = new Writable({ export const defaultLogDestination = new Writable({
objectMode: true, objectMode: true,
write(event: LogMessage, _, callback) { write(event: LogMessage, _, callback) {
let dest: ConsoleStream = process.stderr; let dest: ConsoleStream = process.stderr;
if(levels[event.level] < levels['error']) { if (levels[event.level] < levels['error']) {
dest = process.stdout; dest = process.stdout;
} }
let type = event.type; let type = event.type;
if(event.level === 'info') { if (event.level === 'info') {
type = bold(blue(type)); type = bold(blue(type));
} else if(event.level === 'error') { } else if (event.level === 'error') {
type = bold(red(type)); type = bold(red(type));
} }
dest.write(`[${type}] `); dest.write(`[${type}] `);
dest.write(event.message); dest.write(utilFormat(...event.args));
dest.write('\n'); dest.write('\n');
callback(); callback();
} },
}); });
interface LogWritable<T> extends Writable { interface LogWritable<T> extends Writable {
@ -37,18 +38,19 @@ export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
export interface LogOptions { export interface LogOptions {
dest: LogWritable<LogMessage>; dest: LogWritable<LogMessage>;
level: LoggerLevel level: LoggerLevel;
} }
export const defaultLogOptions: LogOptions = { export const defaultLogOptions: LogOptions = {
dest: defaultLogDestination, dest: defaultLogDestination,
level: 'info' level: 'info',
}; };
export interface LogMessage { export interface LogMessage {
type: string; type: string;
level: LoggerLevel, level: LoggerLevel;
message: string; message: string;
args: Array<any>;
} }
const levels: Record<LoggerLevel, number> = { const levels: Record<LoggerLevel, number> = {
@ -59,19 +61,14 @@ const levels: Record<LoggerLevel, number> = {
silent: 90, silent: 90,
}; };
export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...messages: Array<any>) { export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...args: Array<any>) {
let event: LogMessage = { const event: LogMessage = {
type, type,
level, level,
message: '' args,
message: '',
}; };
if(messages.length === 1 && typeof messages[0] === 'object') {
Object.assign(event, messages[0]);
} else {
event.message = messages.join(' ');
}
// test if this level is enabled or not // test if this level is enabled or not
if (levels[opts.level] > levels[level]) { if (levels[opts.level] > levels[level]) {
return; // do nothing return; // do nothing
@ -99,20 +96,24 @@ export function error(opts: LogOptions, type: string, ...messages: Array<any>) {
export function parseError(opts: LogOptions, err: CompileError) { export function parseError(opts: LogOptions, err: CompileError) {
let frame = err.frame let frame = err.frame
// Switch colons for pipes // Switch colons for pipes
.replace(/^([0-9]+)(:)/mg, `${bold('$1')}`) .replace(/^([0-9]+)(:)/gm, `${bold('$1')}`)
// Make the caret red. // Make the caret red.
.replace(/(?<=^\s+)(\^)/mg, bold(red(' ^'))) .replace(/(?<=^\s+)(\^)/gm, bold(red(' ^')))
// Add identation // Add identation
.replace(/^/mg, ' '); .replace(/^/gm, ' ');
error(opts, 'parse-error', ` error(
opts,
'parse-error',
`
${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))} ${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))}
${bold(red(`𝘅 ${err.message}`))} ${bold(red(`𝘅 ${err.message}`))}
${frame} ${frame}
`); `
);
} }
// A default logger for when too lazy to pass LogOptions around. // A default logger for when too lazy to pass LogOptions around.
@ -120,5 +121,5 @@ export const logger = {
debug: debug.bind(null, defaultLogOptions), debug: debug.bind(null, defaultLogOptions),
info: info.bind(null, defaultLogOptions), info: info.bind(null, defaultLogOptions),
warn: warn.bind(null, defaultLogOptions), warn: warn.bind(null, defaultLogOptions),
error: error.bind(null, defaultLogOptions) error: error.bind(null, defaultLogOptions),
}; };

137
src/runtime.ts Normal file
View 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
View file

View file

@ -0,0 +1,6 @@
export default {
projectRoot: '.',
hmxRoot: './astro',
dist: './_site'
}

View file

@ -0,0 +1,15 @@
<script hmx="setup">
export function setup() {
return {
props: {}
}
}
</script>
<head>
<!-- Head Stuff -->
</head>
<body>
<h1>Hello world!</h1>
</body>

View file

@ -0,0 +1,5 @@
export default {
mount: {
}
};

38
test/hmx-basic.test.js Normal file
View 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
View file

@ -0,0 +1,5 @@
import cheerio from 'cheerio';
export function doc(html) {
return cheerio.load(html);
}