hyper-html-api 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/conformance/MANIFEST.json +2 -2
- package/conformance/cases/all-matches-read-quoted.expected.json +3 -0
- package/conformance/cases/all-matches-read-quoted.html +5 -0
- package/conformance/cases/all-matches-read-quoted.meta +7 -0
- package/conformance/cases/all-matches-read-quoted.parsed.json +3 -0
- package/conformance/cases/all-matches-read.expected.json +3 -0
- package/conformance/cases/all-matches-read.html +6 -0
- package/conformance/cases/all-matches-read.meta +7 -0
- package/conformance/cases/all-matches-read.parsed.json +3 -0
- package/conformance/cases/codec-form-nested.error.json +5 -0
- package/conformance/cases/codec-form-nested.html +4 -0
- package/conformance/cases/codec-form-nested.meta +6 -0
- package/conformance/cases/codec-form-nested.parsed.json +5 -0
- package/conformance/cases/codec-form-star.error.json +5 -0
- package/conformance/cases/codec-form-star.html +4 -0
- package/conformance/cases/codec-form-star.meta +6 -0
- package/conformance/cases/codec-form-star.parsed.json +3 -0
- package/conformance/cases/codec-form-under-v1.error.json +5 -0
- package/conformance/cases/codec-form-under-v1.html +4 -0
- package/conformance/cases/codec-form-under-v1.meta +6 -0
- package/conformance/cases/codec-form-under-v1.parsed.json +3 -0
- package/conformance/cases/codec-unknown-name.error.json +5 -0
- package/conformance/cases/codec-unknown-name.html +4 -0
- package/conformance/cases/codec-unknown-name.meta +6 -0
- package/conformance/cases/codec-unknown-name.parsed.json +3 -0
- package/conformance/cases/tag-version-2-plain.expected.json +7 -0
- package/conformance/cases/tag-version-2-plain.html +5 -0
- package/conformance/cases/tag-version-2-plain.meta +6 -0
- package/conformance/cases/tag-version-2-plain.parsed.json +4 -0
- package/conformance/cases/tag-wrong-version.error.json +1 -1
- package/conformance/cases/tag-wrong-version.html +1 -1
- package/conformance/cases/write-all-matches-attr.after.html +1 -0
- package/conformance/cases/write-all-matches-attr.after.json +3 -0
- package/conformance/cases/write-all-matches-attr.data.json +1 -0
- package/conformance/cases/write-all-matches-attr.html +1 -0
- package/conformance/cases/write-all-matches-attr.meta +6 -0
- package/conformance/cases/write-all-matches-attr.write.json +4 -0
- package/conformance/cases/write-all-matches-group.after.html +1 -0
- package/conformance/cases/write-all-matches-group.after.json +3 -0
- package/conformance/cases/write-all-matches-group.data.json +1 -0
- package/conformance/cases/write-all-matches-group.html +1 -0
- package/conformance/cases/write-all-matches-group.meta +6 -0
- package/conformance/cases/write-all-matches-group.write.json +4 -0
- package/conformance/cases/write-all-matches-text.after.html +1 -0
- package/conformance/cases/write-all-matches-text.after.json +3 -0
- package/conformance/cases/write-all-matches-text.data.json +1 -0
- package/conformance/cases/write-all-matches-text.html +1 -0
- package/conformance/cases/write-all-matches-text.meta +6 -0
- package/conformance/cases/write-all-matches-text.write.json +4 -0
- package/conformance/cases/write-all-matches-unmatched.data.json +1 -0
- package/conformance/cases/write-all-matches-unmatched.error.json +14 -0
- package/conformance/cases/write-all-matches-unmatched.html +1 -0
- package/conformance/cases/write-all-matches-unmatched.meta +6 -0
- package/conformance/sheet/body-data-write.body.json +3 -0
- package/conformance/sheet/body-data-write.data.json +3 -0
- package/conformance/sheet/body-data-write.html +25 -0
- package/conformance/sheet/body-data-write.meta +5 -0
- package/conformance/sheet/body-patch-key-in-rules.body.json +3 -0
- package/conformance/sheet/body-patch-key-in-rules.data.json +13 -0
- package/conformance/sheet/body-patch-key-in-rules.html +18 -0
- package/conformance/sheet/body-patch-key-in-rules.meta +5 -0
- package/conformance/sheet/body-patch-not-array.body.json +3 -0
- package/conformance/sheet/body-patch-not-array.data.json +3 -0
- package/conformance/sheet/body-patch-not-array.html +25 -0
- package/conformance/sheet/body-patch-not-array.meta +5 -0
- package/conformance/sheet/body-patch-on-sheet.body.json +3 -0
- package/conformance/sheet/body-patch-on-sheet.data.json +14 -0
- package/conformance/sheet/body-patch-on-sheet.html +25 -0
- package/conformance/sheet/body-patch-on-sheet.meta +5 -0
- package/conformance/sheet/body-patch-version-1.body.json +3 -0
- package/conformance/sheet/body-patch-version-1.data.json +13 -0
- package/conformance/sheet/body-patch-version-1.html +4 -0
- package/conformance/sheet/body-patch-version-1.meta +5 -0
- package/conformance/sheet/body-patch-with-other-key.body.json +3 -0
- package/conformance/sheet/body-patch-with-other-key.data.json +14 -0
- package/conformance/sheet/body-patch-with-other-key.html +25 -0
- package/conformance/sheet/body-patch-with-other-key.meta +5 -0
- package/conformance/sheet/body-patch-without-operation-id.body.json +3 -0
- package/conformance/sheet/body-patch-without-operation-id.data.json +13 -0
- package/conformance/sheet/body-patch-without-operation-id.html +25 -0
- package/conformance/sheet/body-patch-without-operation-id.meta +5 -0
- package/conformance/sheet/params-bad-range.error.json +6 -0
- package/conformance/sheet/params-bad-range.html +25 -0
- package/conformance/sheet/params-bad-range.meta +6 -0
- package/conformance/sheet/params-bad-render.error.json +6 -0
- package/conformance/sheet/params-bad-render.html +25 -0
- package/conformance/sheet/params-bad-render.meta +6 -0
- package/conformance/sheet/params-bad-shape.error.json +6 -0
- package/conformance/sheet/params-bad-shape.html +25 -0
- package/conformance/sheet/params-bad-shape.meta +6 -0
- package/conformance/sheet/params-cache-buster.expected.json +128 -0
- package/conformance/sheet/params-cache-buster.html +25 -0
- package/conformance/sheet/params-cache-buster.meta +6 -0
- package/conformance/sheet/params-data-alone.expected.json +128 -0
- package/conformance/sheet/params-data-alone.html +25 -0
- package/conformance/sheet/params-data-alone.meta +6 -0
- package/conformance/sheet/params-empty.error.json +6 -0
- package/conformance/sheet/params-empty.html +25 -0
- package/conformance/sheet/params-empty.meta +6 -0
- package/conformance/sheet/params-index.expected.json +19 -0
- package/conformance/sheet/params-index.html +25 -0
- package/conformance/sheet/params-index.meta +6 -0
- package/conformance/sheet/params-none.expected.json +128 -0
- package/conformance/sheet/params-none.html +25 -0
- package/conformance/sheet/params-none.meta +6 -0
- package/conformance/sheet/params-range.expected.json +17 -0
- package/conformance/sheet/params-range.html +25 -0
- package/conformance/sheet/params-range.meta +6 -0
- package/conformance/sheet/params-render-without-range.error.json +6 -0
- package/conformance/sheet/params-render-without-range.html +25 -0
- package/conformance/sheet/params-render-without-range.meta +6 -0
- package/conformance/sheet/params-repeated.error.json +6 -0
- package/conformance/sheet/params-repeated.html +25 -0
- package/conformance/sheet/params-repeated.meta +6 -0
- package/conformance/sheet/params-unknown-sheet.error.json +6 -0
- package/conformance/sheet/params-unknown-sheet.html +25 -0
- package/conformance/sheet/params-unknown-sheet.meta +6 -0
- package/conformance/sheet/params-with-data.error.json +6 -0
- package/conformance/sheet/params-with-data.html +25 -0
- package/conformance/sheet/params-with-data.meta +6 -0
- package/conformance/sheet/sheet-contract-cells.expected.json +57 -0
- package/conformance/sheet/sheet-contract-cells.html +10 -0
- package/conformance/sheet/sheet-contract-cells.meta +6 -0
- package/conformance/sheet/sheet-contract-formulas.expected.json +29 -0
- package/conformance/sheet/sheet-contract-formulas.html +10 -0
- package/conformance/sheet/sheet-contract-formulas.meta +6 -0
- package/conformance/sheet/sheet-contract-values.expected.json +29 -0
- package/conformance/sheet/sheet-contract-values.html +10 -0
- package/conformance/sheet/sheet-contract-values.meta +6 -0
- package/conformance/sheet/sheet-contract-whole.expected.json +59 -0
- package/conformance/sheet/sheet-contract-whole.html +10 -0
- package/conformance/sheet/sheet-contract-whole.meta +5 -0
- package/conformance/sheet/sheet-hostile-01.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-01.html +6 -0
- package/conformance/sheet/sheet-hostile-01.meta +5 -0
- package/conformance/sheet/sheet-hostile-02.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-02.html +6 -0
- package/conformance/sheet/sheet-hostile-02.meta +5 -0
- package/conformance/sheet/sheet-hostile-03.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-03.html +6 -0
- package/conformance/sheet/sheet-hostile-03.meta +5 -0
- package/conformance/sheet/sheet-hostile-04.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-04.html +6 -0
- package/conformance/sheet/sheet-hostile-04.meta +5 -0
- package/conformance/sheet/sheet-hostile-05.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-05.html +6 -0
- package/conformance/sheet/sheet-hostile-05.meta +5 -0
- package/conformance/sheet/sheet-hostile-06.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-06.html +6 -0
- package/conformance/sheet/sheet-hostile-06.meta +5 -0
- package/conformance/sheet/sheet-hostile-07.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-07.html +6 -0
- package/conformance/sheet/sheet-hostile-07.meta +5 -0
- package/conformance/sheet/sheet-hostile-08.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-08.html +6 -0
- package/conformance/sheet/sheet-hostile-08.meta +5 -0
- package/conformance/sheet/sheet-hostile-09.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-09.html +6 -0
- package/conformance/sheet/sheet-hostile-09.meta +5 -0
- package/conformance/sheet/sheet-hostile-10.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-10.html +6 -0
- package/conformance/sheet/sheet-hostile-10.meta +5 -0
- package/conformance/sheet/sheet-hostile-11.expected.json +22 -0
- package/conformance/sheet/sheet-hostile-11.html +6 -0
- package/conformance/sheet/sheet-hostile-11.meta +5 -0
- package/conformance/sheet/sheet-hostile-12.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-12.html +6 -0
- package/conformance/sheet/sheet-hostile-12.meta +5 -0
- package/conformance/sheet/sheet-hostile-13.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-13.html +6 -0
- package/conformance/sheet/sheet-hostile-13.meta +5 -0
- package/conformance/sheet/sheet-hostile-14.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-14.html +6 -0
- package/conformance/sheet/sheet-hostile-14.meta +5 -0
- package/conformance/sheet/sheet-hostile-15.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-15.html +0 -0
- package/conformance/sheet/sheet-hostile-15.meta +5 -0
- package/conformance/sheet/sheet-hostile-16.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-16.html +6 -0
- package/conformance/sheet/sheet-hostile-16.meta +5 -0
- package/conformance/sheet/sheet-hostile-17.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-17.html +6 -0
- package/conformance/sheet/sheet-hostile-17.meta +5 -0
- package/conformance/sheet/sheet-hostile-18.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-18.html +6 -0
- package/conformance/sheet/sheet-hostile-18.meta +5 -0
- package/conformance/sheet/sheet-hostile-19.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-19.html +6 -0
- package/conformance/sheet/sheet-hostile-19.meta +5 -0
- package/conformance/sheet/sheet-hostile-20.expected.json +16 -0
- package/conformance/sheet/sheet-hostile-20.html +6 -0
- package/conformance/sheet/sheet-hostile-20.meta +5 -0
- package/conformance/sheet/sheet-hostile-21.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-21.html +6 -0
- package/conformance/sheet/sheet-hostile-21.meta +5 -0
- package/conformance/sheet/sheet-hostile-22.expected.json +21 -0
- package/conformance/sheet/sheet-hostile-22.html +6 -0
- package/conformance/sheet/sheet-hostile-22.meta +5 -0
- package/conformance/sheet/sheet-hostile-23.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-23.html +6 -0
- package/conformance/sheet/sheet-hostile-23.meta +5 -0
- package/conformance/sheet/sheet-hostile-24.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-24.html +6 -0
- package/conformance/sheet/sheet-hostile-24.meta +5 -0
- package/conformance/sheet/sheet-hostile-25.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-25.html +6 -0
- package/conformance/sheet/sheet-hostile-25.meta +5 -0
- package/conformance/sheet/sheet-hostile-26.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-26.html +6 -0
- package/conformance/sheet/sheet-hostile-26.meta +5 -0
- package/conformance/sheet/sheet-hostile-27.expected.json +24 -0
- package/conformance/sheet/sheet-hostile-27.html +7 -0
- package/conformance/sheet/sheet-hostile-27.meta +5 -0
- package/conformance/sheet/sheet-hostile-28.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-28.html +6 -0
- package/conformance/sheet/sheet-hostile-28.meta +5 -0
- package/conformance/sheet/sheet-hostile-29.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-29.html +6 -0
- package/conformance/sheet/sheet-hostile-29.meta +5 -0
- package/conformance/sheet/sheet-hostile-30.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-30.html +6 -0
- package/conformance/sheet/sheet-hostile-30.meta +5 -0
- package/conformance/sheet/sheet-hostile-31.expected.json +19 -0
- package/conformance/sheet/sheet-hostile-31.html +6 -0
- package/conformance/sheet/sheet-hostile-31.meta +5 -0
- package/conformance/sheet/sheet-hostile-32.expected.json +24 -0
- package/conformance/sheet/sheet-hostile-32.html +6 -0
- package/conformance/sheet/sheet-hostile-32.meta +5 -0
- package/conformance/sheet/sheet-hostile-33.expected.json +18 -0
- package/conformance/sheet/sheet-hostile-33.html +6 -0
- package/conformance/sheet/sheet-hostile-33.meta +5 -0
- package/conformance/sheet/sheet-hostile-34.expected.json +24 -0
- package/conformance/sheet/sheet-hostile-34.html +8 -0
- package/conformance/sheet/sheet-hostile-34.meta +5 -0
- package/conformance/sheet/sheet-hostile-35.expected.json +24 -0
- package/conformance/sheet/sheet-hostile-35.html +8 -0
- package/conformance/sheet/sheet-hostile-35.meta +5 -0
- package/conformance/sheet/sheet-patch-append.after.html +27 -0
- package/conformance/sheet/sheet-patch-append.after.json +143 -0
- package/conformance/sheet/sheet-patch-append.data.json +15 -0
- package/conformance/sheet/sheet-patch-append.html +25 -0
- package/conformance/sheet/sheet-patch-append.meta +5 -0
- package/conformance/sheet/sheet-patch-append.write.json +7 -0
- package/conformance/sheet/sheet-patch-bad-range.data.json +11 -0
- package/conformance/sheet/sheet-patch-bad-range.error.json +6 -0
- package/conformance/sheet/sheet-patch-bad-range.html +25 -0
- package/conformance/sheet/sheet-patch-bad-range.meta +5 -0
- package/conformance/sheet/sheet-patch-bad-value.data.json +13 -0
- package/conformance/sheet/sheet-patch-bad-value.error.json +6 -0
- package/conformance/sheet/sheet-patch-bad-value.html +25 -0
- package/conformance/sheet/sheet-patch-bad-value.meta +5 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.html +25 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.json +129 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.data.json +11 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.html +25 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.meta +5 -0
- package/conformance/sheet/sheet-patch-clean-tab-beside-edited.write.json +7 -0
- package/conformance/sheet/sheet-patch-covered-cell.data.json +10 -0
- package/conformance/sheet/sheet-patch-covered-cell.error.json +6 -0
- package/conformance/sheet/sheet-patch-covered-cell.html +10 -0
- package/conformance/sheet/sheet-patch-covered-cell.meta +5 -0
- package/conformance/sheet/sheet-patch-empty.data.json +1 -0
- package/conformance/sheet/sheet-patch-empty.error.json +6 -0
- package/conformance/sheet/sheet-patch-empty.html +25 -0
- package/conformance/sheet/sheet-patch-empty.meta +5 -0
- package/conformance/sheet/sheet-patch-fallback-route.after.html +24 -0
- package/conformance/sheet/sheet-patch-fallback-route.after.json +86 -0
- package/conformance/sheet/sheet-patch-fallback-route.data.json +10 -0
- package/conformance/sheet/sheet-patch-fallback-route.html +24 -0
- package/conformance/sheet/sheet-patch-fallback-route.meta +5 -0
- package/conformance/sheet/sheet-patch-fallback-route.write.json +7 -0
- package/conformance/sheet/sheet-patch-first-tab.after.html +25 -0
- package/conformance/sheet/sheet-patch-first-tab.after.json +129 -0
- package/conformance/sheet/sheet-patch-first-tab.data.json +10 -0
- package/conformance/sheet/sheet-patch-first-tab.html +25 -0
- package/conformance/sheet/sheet-patch-first-tab.meta +5 -0
- package/conformance/sheet/sheet-patch-first-tab.write.json +7 -0
- package/conformance/sheet/sheet-patch-formula.after.html +25 -0
- package/conformance/sheet/sheet-patch-formula.after.json +128 -0
- package/conformance/sheet/sheet-patch-formula.data.json +11 -0
- package/conformance/sheet/sheet-patch-formula.html +25 -0
- package/conformance/sheet/sheet-patch-formula.meta +5 -0
- package/conformance/sheet/sheet-patch-formula.write.json +7 -0
- package/conformance/sheet/sheet-patch-no-sheet-rule.data.json +10 -0
- package/conformance/sheet/sheet-patch-no-sheet-rule.error.json +6 -0
- package/conformance/sheet/sheet-patch-no-sheet-rule.html +18 -0
- package/conformance/sheet/sheet-patch-no-sheet-rule.meta +5 -0
- package/conformance/sheet/sheet-patch-no-values.data.json +6 -0
- package/conformance/sheet/sheet-patch-no-values.error.json +6 -0
- package/conformance/sheet/sheet-patch-no-values.html +25 -0
- package/conformance/sheet/sheet-patch-no-values.meta +5 -0
- package/conformance/sheet/sheet-patch-not-canonical.data.json +11 -0
- package/conformance/sheet/sheet-patch-not-canonical.error.json +6 -0
- package/conformance/sheet/sheet-patch-not-canonical.html +25 -0
- package/conformance/sheet/sheet-patch-not-canonical.meta +5 -0
- package/conformance/sheet/sheet-patch-outside-sheet.data.json +11 -0
- package/conformance/sheet/sheet-patch-outside-sheet.error.json +6 -0
- package/conformance/sheet/sheet-patch-outside-sheet.html +25 -0
- package/conformance/sheet/sheet-patch-outside-sheet.meta +5 -0
- package/conformance/sheet/sheet-patch-range.after.html +25 -0
- package/conformance/sheet/sheet-patch-range.after.json +126 -0
- package/conformance/sheet/sheet-patch-range.data.json +11 -0
- package/conformance/sheet/sheet-patch-range.html +25 -0
- package/conformance/sheet/sheet-patch-range.meta +5 -0
- package/conformance/sheet/sheet-patch-range.write.json +7 -0
- package/conformance/sheet/sheet-patch-two-tabs.after.html +25 -0
- package/conformance/sheet/sheet-patch-two-tabs.after.json +130 -0
- package/conformance/sheet/sheet-patch-two-tabs.data.json +20 -0
- package/conformance/sheet/sheet-patch-two-tabs.html +25 -0
- package/conformance/sheet/sheet-patch-two-tabs.meta +5 -0
- package/conformance/sheet/sheet-patch-two-tabs.write.json +7 -0
- package/conformance/sheet/sheet-patch-typed.after.html +25 -0
- package/conformance/sheet/sheet-patch-typed.after.json +121 -0
- package/conformance/sheet/sheet-patch-typed.data.json +13 -0
- package/conformance/sheet/sheet-patch-typed.html +25 -0
- package/conformance/sheet/sheet-patch-typed.meta +5 -0
- package/conformance/sheet/sheet-patch-typed.write.json +7 -0
- package/conformance/sheet/sheet-patch-unknown-sheet.data.json +11 -0
- package/conformance/sheet/sheet-patch-unknown-sheet.error.json +6 -0
- package/conformance/sheet/sheet-patch-unknown-sheet.html +25 -0
- package/conformance/sheet/sheet-patch-unknown-sheet.meta +5 -0
- package/conformance/sheet/sheet-read-bad-range.error.json +6 -0
- package/conformance/sheet/sheet-read-bad-range.html +25 -0
- package/conformance/sheet/sheet-read-bad-range.meta +6 -0
- package/conformance/sheet/sheet-read-bad-render.error.json +6 -0
- package/conformance/sheet/sheet-read-bad-render.html +25 -0
- package/conformance/sheet/sheet-read-bad-render.meta +6 -0
- package/conformance/sheet/sheet-read-digest.expected.json +91 -0
- package/conformance/sheet/sheet-read-digest.html +25 -0
- package/conformance/sheet/sheet-read-digest.meta +6 -0
- package/conformance/sheet/sheet-read-empty.expected.json +11 -0
- package/conformance/sheet/sheet-read-empty.html +6 -0
- package/conformance/sheet/sheet-read-empty.meta +5 -0
- package/conformance/sheet/sheet-read-escapes.expected.json +108 -0
- package/conformance/sheet/sheet-read-escapes.html +22 -0
- package/conformance/sheet/sheet-read-escapes.meta +5 -0
- package/conformance/sheet/sheet-read-formulas-formatted.expected.json +298 -0
- package/conformance/sheet/sheet-read-formulas-formatted.html +16 -0
- package/conformance/sheet/sheet-read-formulas-formatted.meta +5 -0
- package/conformance/sheet/sheet-read-grid-in-comment.expected.json +85 -0
- package/conformance/sheet/sheet-read-grid-in-comment.html +24 -0
- package/conformance/sheet/sheet-read-grid-in-comment.meta +5 -0
- package/conformance/sheet/sheet-read-grid-in-script.expected.json +85 -0
- package/conformance/sheet/sheet-read-grid-in-script.html +24 -0
- package/conformance/sheet/sheet-read-grid-in-script.meta +5 -0
- package/conformance/sheet/sheet-read-grid-in-template.expected.json +85 -0
- package/conformance/sheet/sheet-read-grid-in-template.html +24 -0
- package/conformance/sheet/sheet-read-grid-in-template.meta +5 -0
- package/conformance/sheet/sheet-read-grid-in-textarea.expected.json +85 -0
- package/conformance/sheet/sheet-read-grid-in-textarea.html +24 -0
- package/conformance/sheet/sheet-read-grid-in-textarea.meta +5 -0
- package/conformance/sheet/sheet-read-grid-outside-read-by-rule.expected.json +94 -0
- package/conformance/sheet/sheet-read-grid-outside-read-by-rule.html +24 -0
- package/conformance/sheet/sheet-read-grid-outside-read-by-rule.meta +5 -0
- package/conformance/sheet/sheet-read-grid-outside-sheet.expected.json +85 -0
- package/conformance/sheet/sheet-read-grid-outside-sheet.html +24 -0
- package/conformance/sheet/sheet-read-grid-outside-sheet.meta +5 -0
- package/conformance/sheet/sheet-read-hand-edited.expected.json +85 -0
- package/conformance/sheet/sheet-read-hand-edited.html +18 -0
- package/conformance/sheet/sheet-read-hand-edited.meta +5 -0
- package/conformance/sheet/sheet-read-index.expected.json +19 -0
- package/conformance/sheet/sheet-read-index.html +25 -0
- package/conformance/sheet/sheet-read-index.meta +6 -0
- package/conformance/sheet/sheet-read-merged.expected.json +59 -0
- package/conformance/sheet/sheet-read-merged.html +10 -0
- package/conformance/sheet/sheet-read-merged.meta +5 -0
- package/conformance/sheet/sheet-read-no-tag.error.json +5 -0
- package/conformance/sheet/sheet-read-no-tag.html +18 -0
- package/conformance/sheet/sheet-read-no-tag.meta +5 -0
- package/conformance/sheet/sheet-read-padded-name-index.expected.json +19 -0
- package/conformance/sheet/sheet-read-padded-name-index.html +25 -0
- package/conformance/sheet/sheet-read-padded-name-index.meta +6 -0
- package/conformance/sheet/sheet-read-padded-name-lookup.expected.json +83 -0
- package/conformance/sheet/sheet-read-padded-name-lookup.html +25 -0
- package/conformance/sheet/sheet-read-padded-name-lookup.meta +6 -0
- package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.error.json +6 -0
- package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.html +25 -0
- package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.meta +6 -0
- package/conformance/sheet/sheet-read-padded-name.expected.json +128 -0
- package/conformance/sheet/sheet-read-padded-name.html +25 -0
- package/conformance/sheet/sheet-read-padded-name.meta +5 -0
- package/conformance/sheet/sheet-read-range-cells.expected.json +55 -0
- package/conformance/sheet/sheet-read-range-cells.html +25 -0
- package/conformance/sheet/sheet-read-range-cells.meta +6 -0
- package/conformance/sheet/sheet-read-range-first-tab.expected.json +12 -0
- package/conformance/sheet/sheet-read-range-first-tab.html +25 -0
- package/conformance/sheet/sheet-read-range-first-tab.meta +6 -0
- package/conformance/sheet/sheet-read-range-formulas.expected.json +24 -0
- package/conformance/sheet/sheet-read-range-formulas.html +25 -0
- package/conformance/sheet/sheet-read-range-formulas.meta +6 -0
- package/conformance/sheet/sheet-read-range-values.expected.json +24 -0
- package/conformance/sheet/sheet-read-range-values.html +25 -0
- package/conformance/sheet/sheet-read-range-values.meta +6 -0
- package/conformance/sheet/sheet-read-sheet-by-id.expected.json +46 -0
- package/conformance/sheet/sheet-read-sheet-by-id.html +25 -0
- package/conformance/sheet/sheet-read-sheet-by-id.meta +6 -0
- package/conformance/sheet/sheet-read-sheet-by-name.expected.json +46 -0
- package/conformance/sheet/sheet-read-sheet-by-name.html +25 -0
- package/conformance/sheet/sheet-read-sheet-by-name.meta +6 -0
- package/conformance/sheet/sheet-read-styled.expected.json +48 -0
- package/conformance/sheet/sheet-read-styled.html +10 -0
- package/conformance/sheet/sheet-read-styled.meta +5 -0
- package/conformance/sheet/sheet-read-tiny.expected.json +85 -0
- package/conformance/sheet/sheet-read-tiny.html +18 -0
- package/conformance/sheet/sheet-read-tiny.meta +5 -0
- package/conformance/sheet/sheet-read-two-tabs.expected.json +128 -0
- package/conformance/sheet/sheet-read-two-tabs.html +25 -0
- package/conformance/sheet/sheet-read-two-tabs.meta +5 -0
- package/conformance/sheet/sheet-read-unknown-sheet.error.json +6 -0
- package/conformance/sheet/sheet-read-unknown-sheet.html +25 -0
- package/conformance/sheet/sheet-read-unknown-sheet.meta +6 -0
- package/conformance/sheet/sheet-read-version-1-codec.error.json +5 -0
- package/conformance/sheet/sheet-read-version-1-codec.html +25 -0
- package/conformance/sheet/sheet-read-version-1-codec.meta +5 -0
- package/conformance/sheet/sheet-write-whole-refused.data.json +3 -0
- package/conformance/sheet/sheet-write-whole-refused.error.json +7 -0
- package/conformance/sheet/sheet-write-whole-refused.html +25 -0
- package/conformance/sheet/sheet-write-whole-refused.meta +5 -0
- package/conformance/sheet-guard-extraction.json +616 -0
- package/conformance/sheet-guard.json +32362 -0
- package/conformance/sheet-read-shapes.json +1 -0
- package/dist/hyper-html-api.engine.esm.js +1 -1
- package/dist/hyper-html-api.engine.min.js +1 -1
- package/dist/hyper-html-api.min.js +8 -8
- package/dist/hyper-html-api.upgrade.min.js +2 -2
- package/package.json +6 -3
- package/src/codecs/sheet.js +127 -0
- package/src/codecs/sheet.vendor.js +887 -0
- package/src/dom-api.js +20 -11
- package/src/engine/apply.js +17 -7
- package/src/engine/codecs.js +80 -0
- package/src/engine/errors.js +20 -1
- package/src/engine/extract.js +17 -3
- package/src/engine/index.js +9 -5
- package/src/engine/round-trip.js +24 -10
- package/src/engine/rule-syntax.js +29 -3
- package/src/engine/rules-tag.js +3 -2
- package/src/engine/write-plan.js +4 -3
- package/src/page.js +15 -5
- package/src/patch-document.js +122 -0
- package/src/read.js +163 -0
- package/src/site.js +164 -0
- package/src/upgrade/codec-carry.js +23 -0
- package/src/upgrade/extract-all.js +12 -3
- package/src/upgrade/migrate.js +31 -3
- package/src/upgrade/rules-tags.js +4 -4
- package/src/upgrade/shape-match.js +32 -13
- package/src/write.js +44 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import cheerioAdapter from './adapters/cheerio.js'
|
|
2
|
+
import { checkCodecRules } from './engine/codecs.js'
|
|
3
|
+
import { CodecRefused, NoRulesTag } from './engine/errors.js'
|
|
4
|
+
import { findRulesIn } from './engine/rules-tag.js'
|
|
5
|
+
import { codecOf } from './engine/rule-syntax.js'
|
|
6
|
+
import { pickTab, sheet, tabNodes } from './codecs/sheet.js'
|
|
7
|
+
import { CodecMiss, QueryRefused, holeMarker, patchGrid, scanGrid } from './codecs/sheet.vendor.js'
|
|
8
|
+
import { holesHold } from './read.js'
|
|
9
|
+
|
|
10
|
+
export const NOT_CANONICAL_MESSAGE =
|
|
11
|
+
'This sheet was edited outside the sheet editor, so the API cannot patch it: open the sheet in the editor and save once.'
|
|
12
|
+
|
|
13
|
+
// Applies a patch list to a sheet document's source, rewriting only the rows it
|
|
14
|
+
// touches. `load` is cheerio.load. patch is
|
|
15
|
+
// [{ sheet?, range: "B3", values: [[450]] } | { sheet?, append: [["Porto", 12]] }]
|
|
16
|
+
// where `sheet` is a tab name or id (none means the first tab). Values are typed
|
|
17
|
+
// input: null clears, a number or boolean is that value, a string starting with
|
|
18
|
+
// "=" is a formula, any other string is text.
|
|
19
|
+
//
|
|
20
|
+
// options.newId() names appended rows (default: random ids); a test or the
|
|
21
|
+
// conformance runner passes a counter so the output is the same on every run.
|
|
22
|
+
//
|
|
23
|
+
// Returns { html, changed: { cells, rows }, stale: true }: the host cannot
|
|
24
|
+
// recalculate, so every patched table carries `stale` until a browser saves it.
|
|
25
|
+
// Throws before producing output: NoRulesTag; RulesParseError or UnknownCodec for
|
|
26
|
+
// a rules tag that cannot hold a sheet; CodecRefused 'no-sheet' when no single
|
|
27
|
+
// sheet rule matches; QueryRefused (bad_patch, bad_value, bad_range,
|
|
28
|
+
// outside_sheet, covered_cell, unknown_sheet) for a patch the sheet cannot take;
|
|
29
|
+
// CodecRefused 'sheet-not-canonical' (hosts answer 409) for a table edited
|
|
30
|
+
// outside the editor.
|
|
31
|
+
export function patchDocument(load, src, patch, options = {}) {
|
|
32
|
+
const { token = 'api', newId } = options
|
|
33
|
+
if (!Array.isArray(patch) || patch.length === 0) {
|
|
34
|
+
throw new QueryRefused('bad_patch', 'patch is a non-empty list of operations')
|
|
35
|
+
}
|
|
36
|
+
const tabs = locateTabs(load, src, token)
|
|
37
|
+
|
|
38
|
+
const groups = new Map()
|
|
39
|
+
for (const op of patch) {
|
|
40
|
+
if (!op || typeof op !== 'object' || Array.isArray(op)) {
|
|
41
|
+
throw new QueryRefused('bad_patch', 'an operation is { sheet, range, values } or { sheet, append }')
|
|
42
|
+
}
|
|
43
|
+
const { sheet: wanted, ...gridOp } = op
|
|
44
|
+
const tab = pickTab(tabs, wanted)
|
|
45
|
+
if (!groups.has(tab)) groups.set(tab, [])
|
|
46
|
+
groups.get(tab).push(gridOp)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const edits = []
|
|
50
|
+
let cells = 0
|
|
51
|
+
let rows = 0
|
|
52
|
+
for (const [tab, ops] of groups) {
|
|
53
|
+
if (!tab.region) throw new CodecRefused('sheet-not-canonical', NOT_CANONICAL_MESSAGE)
|
|
54
|
+
const { bytes, changed } = patchGrid(src.slice(tab.region.start, tab.region.end), ops, newId ? { newId } : {})
|
|
55
|
+
edits.push({ ...tab.region, bytes })
|
|
56
|
+
cells += changed.cells
|
|
57
|
+
rows += changed.rows
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
edits.sort((a, b) => b.start - a.start)
|
|
61
|
+
let html = src
|
|
62
|
+
for (const edit of edits) html = html.slice(0, edit.start) + edit.bytes + html.slice(edit.end)
|
|
63
|
+
return { html, changed: { cells, rows }, stale: true }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// [{ id, name, region: { start, end } | null }]: region is where the tab's
|
|
67
|
+
// canonical table sits in src, or null when the table is missing or not canonical.
|
|
68
|
+
// The cut route first, as readDocument reads; when its holes do not verify, one
|
|
69
|
+
// parse with source positions, then a strict scan at each table's start.
|
|
70
|
+
function locateTabs(load, src, token) {
|
|
71
|
+
const marker = holeMarker(src)
|
|
72
|
+
const { rest, regions } = sheet.cut(src, marker)
|
|
73
|
+
if (regions.length > 0) {
|
|
74
|
+
const $ = load(rest)
|
|
75
|
+
const root = $.root()
|
|
76
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
77
|
+
if (!found) throw new NoRulesTag(token)
|
|
78
|
+
const container = sheetContainer(root, found)
|
|
79
|
+
if (holesHold($, root, found.rules, 'sheet', marker, regions.length)) {
|
|
80
|
+
return tabNodes(cheerioAdapter, container).map(({ id, name, table }) => ({
|
|
81
|
+
id, name, region: table ? holeRegion(table, marker, regions) : null,
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const root = load(src, { sourceCodeLocationInfo: true }).root()
|
|
87
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
88
|
+
if (!found) throw new NoRulesTag(token)
|
|
89
|
+
const container = sheetContainer(root, found)
|
|
90
|
+
return tabNodes(cheerioAdapter, container).map(({ id, name, table }) => ({
|
|
91
|
+
id, name, region: table ? scannedRegion(table, src) : null,
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sheetContainer(root, found) {
|
|
96
|
+
checkCodecRules(found.rules, { version: found.version, codecs: { sheet } })
|
|
97
|
+
const rules = found.rules
|
|
98
|
+
const forms = rules && typeof rules === 'object' && !Array.isArray(rules)
|
|
99
|
+
? Object.values(rules).map(codecOf).filter((form) => form && form.name === 'sheet')
|
|
100
|
+
: []
|
|
101
|
+
if (forms.length !== 1) throw new CodecRefused('no-sheet', 'this document has no single @codec(sheet) rule to patch')
|
|
102
|
+
const node = forms[0].selector ? cheerioAdapter.find(root, forms[0].selector)[0] : root
|
|
103
|
+
if (!node) throw new CodecRefused('no-sheet', "the document's @codec(sheet) rule matches no element")
|
|
104
|
+
return node
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function holeRegion(table, marker, regions) {
|
|
108
|
+
const hole = cheerioAdapter.attr(table, 'data-hole')
|
|
109
|
+
if (!hole || !hole.startsWith(`${marker}-`)) return null
|
|
110
|
+
return regions[Number(hole.slice(marker.length + 1))] ?? null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scannedRegion(table, src) {
|
|
114
|
+
const start = table.get(0)?.sourceCodeLocation?.startOffset
|
|
115
|
+
if (start == null) return null
|
|
116
|
+
try {
|
|
117
|
+
return { start, end: scanGrid(src, start) }
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof CodecMiss) return null
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/read.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import cheerioAdapter from './adapters/cheerio.js'
|
|
2
|
+
import { extract } from './engine/extract.js'
|
|
3
|
+
import { findRulesIn } from './engine/rules-tag.js'
|
|
4
|
+
import { codecOf } from './engine/rule-syntax.js'
|
|
5
|
+
import { sheet } from './codecs/sheet.js'
|
|
6
|
+
import { checkCodecRules } from './engine/codecs.js'
|
|
7
|
+
import { holeMarker, QueryRefused } from './codecs/sheet.vendor.js'
|
|
8
|
+
|
|
9
|
+
// The URL parameters a host passes to readDocument as `query`, from a
|
|
10
|
+
// URLSearchParams. Returns null when none of them is present (a whole read,
|
|
11
|
+
// exactly as before). Each may appear once with a value, and none may be
|
|
12
|
+
// mixed with ?data= (a caller-supplied rule set); anything else is
|
|
13
|
+
// QueryRefused('bad_query'), which hosts answer with 400. Values are not
|
|
14
|
+
// checked here: readDocument refuses a bad one with its own code.
|
|
15
|
+
export const SHEET_QUERY_PARAMS = ['shape', 'sheet', 'range', 'render']
|
|
16
|
+
|
|
17
|
+
export function parseSheetQuery(params) {
|
|
18
|
+
const query = {}
|
|
19
|
+
for (const key of SHEET_QUERY_PARAMS) {
|
|
20
|
+
const values = params.getAll(key)
|
|
21
|
+
if (values.length === 0) continue
|
|
22
|
+
if (values.length > 1) throw new QueryRefused('bad_query', `${key} appears ${values.length} times; send it once`)
|
|
23
|
+
if (values[0] === '') throw new QueryRefused('bad_query', `${key} is empty; give it a value or leave it out`)
|
|
24
|
+
query[key] = values[0]
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(query).length === 0) return null
|
|
27
|
+
if (params.has('data')) throw new QueryRefused('bad_query', `data cannot be combined with ${Object.keys(query).join(', ')}; a sheet query reads the document's own rules`)
|
|
28
|
+
return query
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Reads a document's data through its rules tag, from source. `load` is
|
|
32
|
+
// cheerio.load, passed in so this package keeps cheerio out of its browser
|
|
33
|
+
// bundles. Returns { data, version, fast }; data is null when no rules tag
|
|
34
|
+
// carries `token`.
|
|
35
|
+
//
|
|
36
|
+
// A codec with cut() lets a host skip parsing its regions: the source is cut,
|
|
37
|
+
// only the rest is parsed, and each hole hands the codec its region's bytes.
|
|
38
|
+
// The holes are verified first: every hole is a real element, each index
|
|
39
|
+
// appears exactly once and in order, and every hole lies under the element a
|
|
40
|
+
// rule naming that codec selected, which only that codec reads. Anything else
|
|
41
|
+
// (a grid inside a comment, a script or a textarea, a grid outside the sheet)
|
|
42
|
+
// discards the cut and reads the whole source through the tree. `fast` is false whenever a
|
|
43
|
+
// `<table grid` was read through the tree (that fallback, or a hand-edited table
|
|
44
|
+
// the cut left in place), so a host can count how often the slow route runs.
|
|
45
|
+
//
|
|
46
|
+
// options: { token = 'api', query, view, codecs = { sheet } }. query and view
|
|
47
|
+
// reach every codec rule (see src/codecs/sheet.js for the sheet's shapes).
|
|
48
|
+
export function readDocument(load, src, options = {}) {
|
|
49
|
+
const { token = 'api', query, view, codecs = { sheet } } = options
|
|
50
|
+
const call = { codecs, query, view }
|
|
51
|
+
const cutting = Object.entries(codecs).filter(([, codec]) => typeof codec.cut === 'function')
|
|
52
|
+
if (cutting.length > 1) throw new Error('hyper-html-api: readDocument supports one codec with cut()')
|
|
53
|
+
|
|
54
|
+
if (cutting.length === 1) {
|
|
55
|
+
const [name, codec] = cutting[0]
|
|
56
|
+
const marker = holeMarker(src)
|
|
57
|
+
const { rest, regions } = codec.cut(src, marker)
|
|
58
|
+
if (regions.length > 0) {
|
|
59
|
+
const $ = load(rest)
|
|
60
|
+
const root = $.root()
|
|
61
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
62
|
+
const fast = countGrids(rest) === regions.length
|
|
63
|
+
if (!found) return { data: null, version: null, fast }
|
|
64
|
+
if (codec === sheet && readHolesHold($, root, found.rules, name, marker, regions.length)) {
|
|
65
|
+
const region = (table) => regionBytes(table, src, marker, regions)
|
|
66
|
+
const opts = { ...call, version: found.version, region }
|
|
67
|
+
checkCodecRules(found.rules, opts)
|
|
68
|
+
let tree
|
|
69
|
+
const data = {}
|
|
70
|
+
for (const [key, rule] of Object.entries(found.rules)) {
|
|
71
|
+
const codec = codecOf(rule)
|
|
72
|
+
const context = codec ? root : (tree ??= load(src).root())
|
|
73
|
+
data[key] = extract(cheerioAdapter, context, { [key]: rule }, { ...opts, region: codec ? region : undefined })[key]
|
|
74
|
+
}
|
|
75
|
+
return { data, version: found.version, fast }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fast = !src.includes(GRID)
|
|
81
|
+
|
|
82
|
+
const root = load(src).root()
|
|
83
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
84
|
+
if (!found) return { data: null, version: null, fast }
|
|
85
|
+
return { data: extract(cheerioAdapter, root, found.rules, { ...call, version: found.version }), version: found.version, fast }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A document's rules tag as { rules, version }, or null when no tag carries
|
|
89
|
+
// `token`, without parsing its canonical tables: what a host needs to classify
|
|
90
|
+
// a request body (isPatchBody) before it reads or patches. Same cut and checks
|
|
91
|
+
// as readDocument; anything they reject is found by parsing the whole source.
|
|
92
|
+
export function documentRules(load, src, options = {}) {
|
|
93
|
+
const { token = 'api', codecs = { sheet } } = options
|
|
94
|
+
const cutting = Object.entries(codecs).filter(([, codec]) => typeof codec.cut === 'function')
|
|
95
|
+
if (cutting.length === 1) {
|
|
96
|
+
const [name, codec] = cutting[0]
|
|
97
|
+
const marker = holeMarker(src)
|
|
98
|
+
const { rest, regions } = codec.cut(src, marker)
|
|
99
|
+
if (regions.length > 0) {
|
|
100
|
+
const $ = load(rest)
|
|
101
|
+
const root = $.root()
|
|
102
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
103
|
+
if (!found) return null
|
|
104
|
+
if (holesHold($, root, found.rules, name, marker, regions.length)) return { rules: found.rules, version: found.version }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const found = findRulesIn(cheerioAdapter, load(src).root(), token)
|
|
108
|
+
return found ? { rules: found.rules, version: found.version } : null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const GRID = '<table grid'
|
|
112
|
+
|
|
113
|
+
function countGrids(text) {
|
|
114
|
+
let n = 0
|
|
115
|
+
for (let at = text.indexOf(GRID); at !== -1; at = text.indexOf(GRID, at + 1)) n++
|
|
116
|
+
return n
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function holesHold($, root, rules, name, marker, count) {
|
|
120
|
+
const holes = $('table[data-hole]').toArray()
|
|
121
|
+
if (holes.length !== count) return false
|
|
122
|
+
const containers = codecContainers(root, rules, name)
|
|
123
|
+
if (containers.length === 0) return false
|
|
124
|
+
return holes.every((hole, n) =>
|
|
125
|
+
hole.attribs['data-hole'] === `${marker}-${n}` &&
|
|
126
|
+
containers.some((container) => container === hole || $.contains(container, hole)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Only this selector cannot match within canonical grid bytes or depend on their children.
|
|
130
|
+
const SHEET_CONTAINER = /^\[items\s*=\s*(?:sheets|"sheets"|'sheets')\]$/
|
|
131
|
+
|
|
132
|
+
function readHolesHold($, root, rules, name, marker, count) {
|
|
133
|
+
if (!rules || typeof rules !== 'object' || Array.isArray(rules)) return false
|
|
134
|
+
for (const rule of Object.values(rules)) {
|
|
135
|
+
const form = codecOf(rule)
|
|
136
|
+
if (form && (form.name !== name || !SHEET_CONTAINER.test(form.selector))) return false
|
|
137
|
+
}
|
|
138
|
+
if (!holesHold($, root, rules, name, marker, count)) return false
|
|
139
|
+
const containers = new Set(codecContainers(root, rules, name))
|
|
140
|
+
return $('table[grid]').length === count && $('table[data-hole]').toArray().every(hole =>
|
|
141
|
+
hole.parent?.name === 'section' && Object.hasOwn(hole.parent.attribs, 'item') && containers.has(hole.parent.parent))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The element each top level `selector@codec(name)` rule selects, as the engine
|
|
145
|
+
// would read it: the first match, or the document itself for a bare "@codec(name)".
|
|
146
|
+
function codecContainers(root, rules, name) {
|
|
147
|
+
if (!rules || typeof rules !== 'object' || Array.isArray(rules)) return []
|
|
148
|
+
const out = []
|
|
149
|
+
for (const rule of Object.values(rules)) {
|
|
150
|
+
const form = codecOf(rule)
|
|
151
|
+
if (!form || form.name !== name) continue
|
|
152
|
+
const node = form.selector ? cheerioAdapter.find(root, form.selector)[0] : root
|
|
153
|
+
if (node && node.get(0)) out.push(node.get(0))
|
|
154
|
+
}
|
|
155
|
+
return out
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function regionBytes(table, src, marker, regions) {
|
|
159
|
+
const hole = cheerioAdapter.attr(table, 'data-hole')
|
|
160
|
+
if (!hole || !hole.startsWith(`${marker}-`)) return null
|
|
161
|
+
const region = regions[Number(hole.slice(marker.length + 1))]
|
|
162
|
+
return region ? src.slice(region.start, region.end) : null
|
|
163
|
+
}
|
package/src/site.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import cheerioAdapter from './adapters/cheerio.js'
|
|
2
|
+
import { extract } from './engine/extract.js'
|
|
3
|
+
import { findRulesIn } from './engine/rules-tag.js'
|
|
4
|
+
import { splitRule } from './engine/rule-syntax.js'
|
|
5
|
+
import { writeDocument } from './write.js'
|
|
6
|
+
|
|
7
|
+
export const SITE_KEY = 'site'
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The part of `value` that `rule` declares. Keys the rule does not name are
|
|
14
|
+
// dropped at every level, because the writer refuses unknown keys and a store
|
|
15
|
+
// holds keys other pages use. A key the store does not hold stays absent, and
|
|
16
|
+
// the writer skips an absent key, so the page keeps what it has.
|
|
17
|
+
export function projectSite(rule, value) {
|
|
18
|
+
if (isPlainObject(rule)) {
|
|
19
|
+
if (!isPlainObject(value)) return value
|
|
20
|
+
const out = {}
|
|
21
|
+
for (const key of Object.keys(rule)) {
|
|
22
|
+
if (value[key] !== undefined) out[key] = projectSite(rule[key], value[key])
|
|
23
|
+
}
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(rule) && Array.isArray(value)) {
|
|
27
|
+
return value.map((item) => projectSite(rule[1], item))
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Writes the store's values into a page through the page's own `site` rule.
|
|
33
|
+
// Never throws for a page problem: a key that cannot be written is listed in
|
|
34
|
+
// `failed` and the other keys still land. `declared` is false when the page has
|
|
35
|
+
// no rules tag or no `site` object in it, and then `html` is `src`.
|
|
36
|
+
export function writeSite(load, src, store, options = {}) {
|
|
37
|
+
const { token = 'api' } = options
|
|
38
|
+
const none = { html: src, changed: false, declared: false, applied: [], failed: [] }
|
|
39
|
+
if (!isPlainObject(store)) return none
|
|
40
|
+
let found
|
|
41
|
+
try {
|
|
42
|
+
found = findRulesIn(cheerioAdapter, load(src).root(), token)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { ...none, declared: true, failed: [{ key: null, code: error.name, message: error.message }] }
|
|
45
|
+
}
|
|
46
|
+
if (!found || !isPlainObject(found.rules) || !isPlainObject(found.rules[SITE_KEY])) return none
|
|
47
|
+
const data = projectSite(found.rules[SITE_KEY], store)
|
|
48
|
+
const keys = Object.keys(data)
|
|
49
|
+
if (keys.length === 0) return { ...none, declared: true }
|
|
50
|
+
const write = (html, part) => writeDocument(load, html, { [SITE_KEY]: part }, { token, rules: found.rules }).html
|
|
51
|
+
try {
|
|
52
|
+
const html = write(src, data)
|
|
53
|
+
return { html, changed: html !== src, declared: true, applied: keys, failed: [] }
|
|
54
|
+
} catch {
|
|
55
|
+
// One key failed. Write them one at a time so the rest still land.
|
|
56
|
+
}
|
|
57
|
+
let html = src
|
|
58
|
+
const applied = []
|
|
59
|
+
const failed = []
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
try {
|
|
62
|
+
html = write(html, { [key]: data[key] })
|
|
63
|
+
applied.push(key)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
failed.push({ key, code: error.name, message: error.message })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { html, changed: html !== src, declared: true, applied, failed }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The same rule with each attribute read turned into a read of its element's text, which is
|
|
72
|
+
// null exactly where the element is missing: a missing element can then be told apart from
|
|
73
|
+
// an element without the attribute, which both read as a null attribute.
|
|
74
|
+
function presenceRule(rule) {
|
|
75
|
+
if (typeof rule === 'string') {
|
|
76
|
+
const { selector, prop } = splitRule(rule)
|
|
77
|
+
return prop && !prop.endsWith('[]') ? (selector || '.') : rule
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(rule)) return rule.length < 2 ? rule : [rule[0], presenceRule(rule[1])]
|
|
80
|
+
if (isPlainObject(rule)) return Object.fromEntries(Object.entries(rule).map(([key, value]) => [key, presenceRule(value)]))
|
|
81
|
+
return rule
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The page's own `site` rule and the values the page holds for it right now, read key by
|
|
85
|
+
// key: a key whose rule cannot be read (a broken selector) is listed in `broken` and left
|
|
86
|
+
// out of `data`, and the other keys still read. `present` mirrors `data` for
|
|
87
|
+
// siteValueProblem. Never throws: a page with no rules tag, or no `site` key, declares nothing.
|
|
88
|
+
export function readSite(load, src, options = {}) {
|
|
89
|
+
const { token = 'api' } = options
|
|
90
|
+
const none = { declared: false, rule: null, data: {}, present: {}, broken: [] }
|
|
91
|
+
let root
|
|
92
|
+
let found
|
|
93
|
+
try {
|
|
94
|
+
root = load(src).root()
|
|
95
|
+
found = findRulesIn(cheerioAdapter, root, token)
|
|
96
|
+
} catch {
|
|
97
|
+
return none
|
|
98
|
+
}
|
|
99
|
+
if (!found || !isPlainObject(found.rules) || !isPlainObject(found.rules[SITE_KEY])) return none
|
|
100
|
+
const rule = found.rules[SITE_KEY]
|
|
101
|
+
const read = (keyRule, key) => extract(cheerioAdapter, root, { [key]: keyRule }, { templateAttr: 'cms-template' })[key]
|
|
102
|
+
const data = {}
|
|
103
|
+
const present = {}
|
|
104
|
+
const broken = []
|
|
105
|
+
for (const key of Object.keys(rule)) {
|
|
106
|
+
try {
|
|
107
|
+
data[key] = read(rule[key], key)
|
|
108
|
+
present[key] = read(presenceRule(rule[key]), key)
|
|
109
|
+
} catch {
|
|
110
|
+
delete data[key]
|
|
111
|
+
broken.push(key)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { declared: true, rule, data, present, broken }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Why a value read from a page must not be written to the store, or null when it may.
|
|
118
|
+
// A text rule that matched no element reads null, and an emptied list cannot be regrown
|
|
119
|
+
// by writeSite, so both would damage every other page. A null attribute is a real value:
|
|
120
|
+
// it means the attribute is absent, unless `present` (readSite's mirror of the value) shows
|
|
121
|
+
// that the attribute's element is missing too.
|
|
122
|
+
export function siteValueProblem(rule, value, present) {
|
|
123
|
+
if (typeof rule === 'string') {
|
|
124
|
+
if (rule.endsWith('[]')) {
|
|
125
|
+
if (!Array.isArray(value)) return 'unreadable'
|
|
126
|
+
return value.length === 0 ? 'empty-list' : null
|
|
127
|
+
}
|
|
128
|
+
if (value !== null) return null
|
|
129
|
+
return splitRule(rule).prop === null || present === null ? 'unreadable' : null
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(rule)) {
|
|
132
|
+
if (!Array.isArray(value)) return 'unreadable'
|
|
133
|
+
if (value.length === 0) return 'empty-list'
|
|
134
|
+
if (rule.length < 2) return null
|
|
135
|
+
for (const [index, row] of value.entries()) {
|
|
136
|
+
const problem = siteValueProblem(rule[1], row, Array.isArray(present) ? present[index] : undefined)
|
|
137
|
+
if (problem) return problem
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
if (isPlainObject(rule)) {
|
|
142
|
+
if (!isPlainObject(value)) return 'unreadable'
|
|
143
|
+
for (const key of Object.keys(rule)) {
|
|
144
|
+
const problem = siteValueProblem(rule[key], value[key], isPlainObject(present) ? present[key] : undefined)
|
|
145
|
+
if (problem) return problem
|
|
146
|
+
}
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
return 'unreadable'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The store's next value for one key after a page edit. An object keeps the fields the
|
|
153
|
+
// page's rule does not name, because other pages use them. A list is taken whole from
|
|
154
|
+
// the page: rows have no identity to merge by.
|
|
155
|
+
export function mergeSiteEdit(rule, stored, edited) {
|
|
156
|
+
if (isPlainObject(rule) && isPlainObject(edited)) {
|
|
157
|
+
const out = isPlainObject(stored) ? { ...stored } : {}
|
|
158
|
+
for (const key of Object.keys(rule)) {
|
|
159
|
+
if (edited[key] !== undefined) out[key] = mergeSiteEdit(rule[key], out[key], edited[key])
|
|
160
|
+
}
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
return edited
|
|
164
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { codecOf } from '../engine/rule-syntax.js'
|
|
2
|
+
|
|
3
|
+
// Under a version 2 rules tag, a top level `@codec(name)` key (a sheet) is
|
|
4
|
+
// carried whole on upgrade: the extract side keeps its container's markup as
|
|
5
|
+
// a string, and the migrate side puts that string into the same key's
|
|
6
|
+
// container in the new template. The value is never read into data and
|
|
7
|
+
// written back, so the codec needs no whole-value write and the canonical
|
|
8
|
+
// bytes its grammar reads stay as they were.
|
|
9
|
+
export function splitCarried(rules, version) {
|
|
10
|
+
if (version !== '2' || !rules || typeof rules !== 'object' || Array.isArray(rules)) return { rules, carried: [] }
|
|
11
|
+
const rest = {}
|
|
12
|
+
const carried = []
|
|
13
|
+
for (const [key, rule] of Object.entries(rules)) {
|
|
14
|
+
const codec = codecOf(rule)
|
|
15
|
+
if (codec) carried.push({ key, selector: codec.selector })
|
|
16
|
+
else rest[key] = rule
|
|
17
|
+
}
|
|
18
|
+
return { rules: rest, carried }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function carriedNode(adapter, root, selector) {
|
|
22
|
+
return adapter.find(root, selector)[0] || null
|
|
23
|
+
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { extract } from '../engine/extract.js'
|
|
2
2
|
import domAdapter from '../adapters/dom.js'
|
|
3
3
|
import { collectRulesTags } from './rules-tags.js'
|
|
4
|
+
import { splitCarried, carriedNode } from './codec-carry.js'
|
|
4
5
|
import { readMeta } from './check.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Keyed extraction: one entry per rules tag under `root`, keyed by the
|
|
8
9
|
* normalized data-rules-name value. This is the v1 side of an upgrade:
|
|
9
|
-
* pack the page's data into labeled boxes.
|
|
10
|
+
* pack the page's data into labeled boxes. A `@codec(name)` key (a sheet,
|
|
11
|
+
* version 2 tags only) packs as its container's markup string, which a
|
|
12
|
+
* transform passes through unchanged.
|
|
10
13
|
*/
|
|
11
14
|
export function extractAllFrom(adapter, root) {
|
|
12
15
|
const dataByName = {}
|
|
13
|
-
for (const { name, rules } of collectRulesTags(adapter, root)) {
|
|
14
|
-
|
|
16
|
+
for (const { name, rules, version } of collectRulesTags(adapter, root)) {
|
|
17
|
+
const { rules: plain, carried } = splitCarried(rules, version)
|
|
18
|
+
const data = extract(adapter, root, plain, { version })
|
|
19
|
+
for (const { key, selector } of carried) {
|
|
20
|
+
const node = carriedNode(adapter, root, selector)
|
|
21
|
+
data[key] = node ? adapter.prop(node, 'innerHTML') : null
|
|
22
|
+
}
|
|
23
|
+
dataByName[name] = data
|
|
15
24
|
}
|
|
16
25
|
return dataByName
|
|
17
26
|
}
|
package/src/upgrade/migrate.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { apply } from '../engine/apply.js'
|
|
2
2
|
import { collectRulesTags } from './rules-tags.js'
|
|
3
3
|
import { shapeMatch, countScalarLeaves } from './shape-match.js'
|
|
4
|
+
import { splitCarried, carriedNode } from './codec-carry.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Pour keyed v1 data into the v2 document. Exact-name join: each v2 rules
|
|
7
8
|
* tag pulls the entry with the same normalized name; tags with no matching
|
|
8
9
|
* entry keep their template defaults; v1 entries with no matching tag are
|
|
9
10
|
* counted as discarded. Renames are the transform's job, not the join's.
|
|
11
|
+
* A `@codec(name)` key takes its markup string whole into the new
|
|
12
|
+
* template's container for that key.
|
|
10
13
|
*/
|
|
11
14
|
export function migrateInto(adapter, root, dataByName) {
|
|
12
15
|
const tags = collectRulesTags(adapter, root)
|
|
@@ -14,14 +17,28 @@ export function migrateInto(adapter, root, dataByName) {
|
|
|
14
17
|
const byName = {}
|
|
15
18
|
const v2Names = new Set()
|
|
16
19
|
|
|
17
|
-
for (const { name, rules } of tags) {
|
|
20
|
+
for (const { name, rules, version } of tags) {
|
|
18
21
|
v2Names.add(name)
|
|
19
22
|
if (!Object.prototype.hasOwnProperty.call(dataByName, name)) {
|
|
20
23
|
byName[name] = { carriedOver: 0, discarded: 0, listItems: 0 }
|
|
21
24
|
continue
|
|
22
25
|
}
|
|
23
|
-
const {
|
|
24
|
-
|
|
26
|
+
const { rules: plain, carried } = splitCarried(rules, version)
|
|
27
|
+
const entry = dataByName[name]
|
|
28
|
+
const { data, summary } = shapeMatch(withoutCarried(entry, carried), plain)
|
|
29
|
+
// A row whose nested list is empty at that spot in v2 grows from the page's seeds, as a write does.
|
|
30
|
+
apply(adapter, root, plain, data, { templateAttr: 'cms-template' })
|
|
31
|
+
for (const { key, selector } of carried) {
|
|
32
|
+
const markup = isPlainObject(entry) ? entry[key] : undefined
|
|
33
|
+
if (markup === undefined || markup === null) continue
|
|
34
|
+
const node = carriedNode(adapter, root, selector)
|
|
35
|
+
if (typeof markup === 'string' && node) {
|
|
36
|
+
adapter.prop(node, 'innerHTML', markup)
|
|
37
|
+
summary.carriedOver += 1
|
|
38
|
+
} else {
|
|
39
|
+
summary.discarded += 1
|
|
40
|
+
}
|
|
41
|
+
}
|
|
25
42
|
byName[name] = summary
|
|
26
43
|
totals.carriedOver += summary.carriedOver
|
|
27
44
|
totals.discarded += summary.discarded
|
|
@@ -34,3 +51,14 @@ export function migrateInto(adapter, root, dataByName) {
|
|
|
34
51
|
|
|
35
52
|
return { totals, byName, rulesTagCount: tags.length }
|
|
36
53
|
}
|
|
54
|
+
|
|
55
|
+
function isPlainObject(value) {
|
|
56
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function withoutCarried(entry, carried) {
|
|
60
|
+
if (!carried.length || !isPlainObject(entry)) return entry
|
|
61
|
+
const rest = { ...entry }
|
|
62
|
+
for (const { key } of carried) delete rest[key]
|
|
63
|
+
return rest
|
|
64
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseRelaxed } from '../engine/rules.js'
|
|
2
|
-
import {
|
|
2
|
+
import { SUPPORTED_VERSIONS } from '../engine/rules-tag.js'
|
|
3
3
|
import { UnknownRulesVersion } from '../engine/errors.js'
|
|
4
4
|
|
|
5
5
|
const RULES_SELECTOR = 'script[data-rules-name]'
|
|
@@ -9,7 +9,7 @@ export function normalizeName(raw) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Collect every rules tag under `root` as [{ name, rules }], keyed by the
|
|
12
|
+
* Collect every rules tag under `root` as [{ name, rules, version }], keyed by the
|
|
13
13
|
* whitespace-normalized data-rules-name attribute value. Duplicate names:
|
|
14
14
|
* first tag wins with a warning (mirrors findRulesIn). Unsupported
|
|
15
15
|
* data-rules-version throws so engine-format skew fails loudly instead of
|
|
@@ -29,9 +29,9 @@ export function collectRulesTags(adapter, root) {
|
|
|
29
29
|
continue
|
|
30
30
|
}
|
|
31
31
|
const version = adapter.attr(tag, 'data-rules-version')
|
|
32
|
-
if (version
|
|
32
|
+
if (!SUPPORTED_VERSIONS.includes(version)) throw new UnknownRulesVersion(version)
|
|
33
33
|
seen.add(name)
|
|
34
|
-
out.push({ name, rules: parseRelaxed(adapter.text(tag)) })
|
|
34
|
+
out.push({ name, rules: parseRelaxed(adapter.text(tag)), version })
|
|
35
35
|
}
|
|
36
36
|
return out
|
|
37
37
|
}
|