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.
Files changed (449) hide show
  1. package/README.md +4 -1
  2. package/conformance/MANIFEST.json +2 -2
  3. package/conformance/cases/all-matches-read-quoted.expected.json +3 -0
  4. package/conformance/cases/all-matches-read-quoted.html +5 -0
  5. package/conformance/cases/all-matches-read-quoted.meta +7 -0
  6. package/conformance/cases/all-matches-read-quoted.parsed.json +3 -0
  7. package/conformance/cases/all-matches-read.expected.json +3 -0
  8. package/conformance/cases/all-matches-read.html +6 -0
  9. package/conformance/cases/all-matches-read.meta +7 -0
  10. package/conformance/cases/all-matches-read.parsed.json +3 -0
  11. package/conformance/cases/codec-form-nested.error.json +5 -0
  12. package/conformance/cases/codec-form-nested.html +4 -0
  13. package/conformance/cases/codec-form-nested.meta +6 -0
  14. package/conformance/cases/codec-form-nested.parsed.json +5 -0
  15. package/conformance/cases/codec-form-star.error.json +5 -0
  16. package/conformance/cases/codec-form-star.html +4 -0
  17. package/conformance/cases/codec-form-star.meta +6 -0
  18. package/conformance/cases/codec-form-star.parsed.json +3 -0
  19. package/conformance/cases/codec-form-under-v1.error.json +5 -0
  20. package/conformance/cases/codec-form-under-v1.html +4 -0
  21. package/conformance/cases/codec-form-under-v1.meta +6 -0
  22. package/conformance/cases/codec-form-under-v1.parsed.json +3 -0
  23. package/conformance/cases/codec-unknown-name.error.json +5 -0
  24. package/conformance/cases/codec-unknown-name.html +4 -0
  25. package/conformance/cases/codec-unknown-name.meta +6 -0
  26. package/conformance/cases/codec-unknown-name.parsed.json +3 -0
  27. package/conformance/cases/tag-version-2-plain.expected.json +7 -0
  28. package/conformance/cases/tag-version-2-plain.html +5 -0
  29. package/conformance/cases/tag-version-2-plain.meta +6 -0
  30. package/conformance/cases/tag-version-2-plain.parsed.json +4 -0
  31. package/conformance/cases/tag-wrong-version.error.json +1 -1
  32. package/conformance/cases/tag-wrong-version.html +1 -1
  33. package/conformance/cases/write-all-matches-attr.after.html +1 -0
  34. package/conformance/cases/write-all-matches-attr.after.json +3 -0
  35. package/conformance/cases/write-all-matches-attr.data.json +1 -0
  36. package/conformance/cases/write-all-matches-attr.html +1 -0
  37. package/conformance/cases/write-all-matches-attr.meta +6 -0
  38. package/conformance/cases/write-all-matches-attr.write.json +4 -0
  39. package/conformance/cases/write-all-matches-group.after.html +1 -0
  40. package/conformance/cases/write-all-matches-group.after.json +3 -0
  41. package/conformance/cases/write-all-matches-group.data.json +1 -0
  42. package/conformance/cases/write-all-matches-group.html +1 -0
  43. package/conformance/cases/write-all-matches-group.meta +6 -0
  44. package/conformance/cases/write-all-matches-group.write.json +4 -0
  45. package/conformance/cases/write-all-matches-text.after.html +1 -0
  46. package/conformance/cases/write-all-matches-text.after.json +3 -0
  47. package/conformance/cases/write-all-matches-text.data.json +1 -0
  48. package/conformance/cases/write-all-matches-text.html +1 -0
  49. package/conformance/cases/write-all-matches-text.meta +6 -0
  50. package/conformance/cases/write-all-matches-text.write.json +4 -0
  51. package/conformance/cases/write-all-matches-unmatched.data.json +1 -0
  52. package/conformance/cases/write-all-matches-unmatched.error.json +14 -0
  53. package/conformance/cases/write-all-matches-unmatched.html +1 -0
  54. package/conformance/cases/write-all-matches-unmatched.meta +6 -0
  55. package/conformance/sheet/body-data-write.body.json +3 -0
  56. package/conformance/sheet/body-data-write.data.json +3 -0
  57. package/conformance/sheet/body-data-write.html +25 -0
  58. package/conformance/sheet/body-data-write.meta +5 -0
  59. package/conformance/sheet/body-patch-key-in-rules.body.json +3 -0
  60. package/conformance/sheet/body-patch-key-in-rules.data.json +13 -0
  61. package/conformance/sheet/body-patch-key-in-rules.html +18 -0
  62. package/conformance/sheet/body-patch-key-in-rules.meta +5 -0
  63. package/conformance/sheet/body-patch-not-array.body.json +3 -0
  64. package/conformance/sheet/body-patch-not-array.data.json +3 -0
  65. package/conformance/sheet/body-patch-not-array.html +25 -0
  66. package/conformance/sheet/body-patch-not-array.meta +5 -0
  67. package/conformance/sheet/body-patch-on-sheet.body.json +3 -0
  68. package/conformance/sheet/body-patch-on-sheet.data.json +14 -0
  69. package/conformance/sheet/body-patch-on-sheet.html +25 -0
  70. package/conformance/sheet/body-patch-on-sheet.meta +5 -0
  71. package/conformance/sheet/body-patch-version-1.body.json +3 -0
  72. package/conformance/sheet/body-patch-version-1.data.json +13 -0
  73. package/conformance/sheet/body-patch-version-1.html +4 -0
  74. package/conformance/sheet/body-patch-version-1.meta +5 -0
  75. package/conformance/sheet/body-patch-with-other-key.body.json +3 -0
  76. package/conformance/sheet/body-patch-with-other-key.data.json +14 -0
  77. package/conformance/sheet/body-patch-with-other-key.html +25 -0
  78. package/conformance/sheet/body-patch-with-other-key.meta +5 -0
  79. package/conformance/sheet/body-patch-without-operation-id.body.json +3 -0
  80. package/conformance/sheet/body-patch-without-operation-id.data.json +13 -0
  81. package/conformance/sheet/body-patch-without-operation-id.html +25 -0
  82. package/conformance/sheet/body-patch-without-operation-id.meta +5 -0
  83. package/conformance/sheet/params-bad-range.error.json +6 -0
  84. package/conformance/sheet/params-bad-range.html +25 -0
  85. package/conformance/sheet/params-bad-range.meta +6 -0
  86. package/conformance/sheet/params-bad-render.error.json +6 -0
  87. package/conformance/sheet/params-bad-render.html +25 -0
  88. package/conformance/sheet/params-bad-render.meta +6 -0
  89. package/conformance/sheet/params-bad-shape.error.json +6 -0
  90. package/conformance/sheet/params-bad-shape.html +25 -0
  91. package/conformance/sheet/params-bad-shape.meta +6 -0
  92. package/conformance/sheet/params-cache-buster.expected.json +128 -0
  93. package/conformance/sheet/params-cache-buster.html +25 -0
  94. package/conformance/sheet/params-cache-buster.meta +6 -0
  95. package/conformance/sheet/params-data-alone.expected.json +128 -0
  96. package/conformance/sheet/params-data-alone.html +25 -0
  97. package/conformance/sheet/params-data-alone.meta +6 -0
  98. package/conformance/sheet/params-empty.error.json +6 -0
  99. package/conformance/sheet/params-empty.html +25 -0
  100. package/conformance/sheet/params-empty.meta +6 -0
  101. package/conformance/sheet/params-index.expected.json +19 -0
  102. package/conformance/sheet/params-index.html +25 -0
  103. package/conformance/sheet/params-index.meta +6 -0
  104. package/conformance/sheet/params-none.expected.json +128 -0
  105. package/conformance/sheet/params-none.html +25 -0
  106. package/conformance/sheet/params-none.meta +6 -0
  107. package/conformance/sheet/params-range.expected.json +17 -0
  108. package/conformance/sheet/params-range.html +25 -0
  109. package/conformance/sheet/params-range.meta +6 -0
  110. package/conformance/sheet/params-render-without-range.error.json +6 -0
  111. package/conformance/sheet/params-render-without-range.html +25 -0
  112. package/conformance/sheet/params-render-without-range.meta +6 -0
  113. package/conformance/sheet/params-repeated.error.json +6 -0
  114. package/conformance/sheet/params-repeated.html +25 -0
  115. package/conformance/sheet/params-repeated.meta +6 -0
  116. package/conformance/sheet/params-unknown-sheet.error.json +6 -0
  117. package/conformance/sheet/params-unknown-sheet.html +25 -0
  118. package/conformance/sheet/params-unknown-sheet.meta +6 -0
  119. package/conformance/sheet/params-with-data.error.json +6 -0
  120. package/conformance/sheet/params-with-data.html +25 -0
  121. package/conformance/sheet/params-with-data.meta +6 -0
  122. package/conformance/sheet/sheet-contract-cells.expected.json +57 -0
  123. package/conformance/sheet/sheet-contract-cells.html +10 -0
  124. package/conformance/sheet/sheet-contract-cells.meta +6 -0
  125. package/conformance/sheet/sheet-contract-formulas.expected.json +29 -0
  126. package/conformance/sheet/sheet-contract-formulas.html +10 -0
  127. package/conformance/sheet/sheet-contract-formulas.meta +6 -0
  128. package/conformance/sheet/sheet-contract-values.expected.json +29 -0
  129. package/conformance/sheet/sheet-contract-values.html +10 -0
  130. package/conformance/sheet/sheet-contract-values.meta +6 -0
  131. package/conformance/sheet/sheet-contract-whole.expected.json +59 -0
  132. package/conformance/sheet/sheet-contract-whole.html +10 -0
  133. package/conformance/sheet/sheet-contract-whole.meta +5 -0
  134. package/conformance/sheet/sheet-hostile-01.expected.json +18 -0
  135. package/conformance/sheet/sheet-hostile-01.html +6 -0
  136. package/conformance/sheet/sheet-hostile-01.meta +5 -0
  137. package/conformance/sheet/sheet-hostile-02.expected.json +18 -0
  138. package/conformance/sheet/sheet-hostile-02.html +6 -0
  139. package/conformance/sheet/sheet-hostile-02.meta +5 -0
  140. package/conformance/sheet/sheet-hostile-03.expected.json +18 -0
  141. package/conformance/sheet/sheet-hostile-03.html +6 -0
  142. package/conformance/sheet/sheet-hostile-03.meta +5 -0
  143. package/conformance/sheet/sheet-hostile-04.expected.json +18 -0
  144. package/conformance/sheet/sheet-hostile-04.html +6 -0
  145. package/conformance/sheet/sheet-hostile-04.meta +5 -0
  146. package/conformance/sheet/sheet-hostile-05.expected.json +18 -0
  147. package/conformance/sheet/sheet-hostile-05.html +6 -0
  148. package/conformance/sheet/sheet-hostile-05.meta +5 -0
  149. package/conformance/sheet/sheet-hostile-06.expected.json +21 -0
  150. package/conformance/sheet/sheet-hostile-06.html +6 -0
  151. package/conformance/sheet/sheet-hostile-06.meta +5 -0
  152. package/conformance/sheet/sheet-hostile-07.expected.json +21 -0
  153. package/conformance/sheet/sheet-hostile-07.html +6 -0
  154. package/conformance/sheet/sheet-hostile-07.meta +5 -0
  155. package/conformance/sheet/sheet-hostile-08.expected.json +21 -0
  156. package/conformance/sheet/sheet-hostile-08.html +6 -0
  157. package/conformance/sheet/sheet-hostile-08.meta +5 -0
  158. package/conformance/sheet/sheet-hostile-09.expected.json +21 -0
  159. package/conformance/sheet/sheet-hostile-09.html +6 -0
  160. package/conformance/sheet/sheet-hostile-09.meta +5 -0
  161. package/conformance/sheet/sheet-hostile-10.expected.json +21 -0
  162. package/conformance/sheet/sheet-hostile-10.html +6 -0
  163. package/conformance/sheet/sheet-hostile-10.meta +5 -0
  164. package/conformance/sheet/sheet-hostile-11.expected.json +22 -0
  165. package/conformance/sheet/sheet-hostile-11.html +6 -0
  166. package/conformance/sheet/sheet-hostile-11.meta +5 -0
  167. package/conformance/sheet/sheet-hostile-12.expected.json +18 -0
  168. package/conformance/sheet/sheet-hostile-12.html +6 -0
  169. package/conformance/sheet/sheet-hostile-12.meta +5 -0
  170. package/conformance/sheet/sheet-hostile-13.expected.json +18 -0
  171. package/conformance/sheet/sheet-hostile-13.html +6 -0
  172. package/conformance/sheet/sheet-hostile-13.meta +5 -0
  173. package/conformance/sheet/sheet-hostile-14.expected.json +21 -0
  174. package/conformance/sheet/sheet-hostile-14.html +6 -0
  175. package/conformance/sheet/sheet-hostile-14.meta +5 -0
  176. package/conformance/sheet/sheet-hostile-15.expected.json +18 -0
  177. package/conformance/sheet/sheet-hostile-15.html +0 -0
  178. package/conformance/sheet/sheet-hostile-15.meta +5 -0
  179. package/conformance/sheet/sheet-hostile-16.expected.json +18 -0
  180. package/conformance/sheet/sheet-hostile-16.html +6 -0
  181. package/conformance/sheet/sheet-hostile-16.meta +5 -0
  182. package/conformance/sheet/sheet-hostile-17.expected.json +18 -0
  183. package/conformance/sheet/sheet-hostile-17.html +6 -0
  184. package/conformance/sheet/sheet-hostile-17.meta +5 -0
  185. package/conformance/sheet/sheet-hostile-18.expected.json +18 -0
  186. package/conformance/sheet/sheet-hostile-18.html +6 -0
  187. package/conformance/sheet/sheet-hostile-18.meta +5 -0
  188. package/conformance/sheet/sheet-hostile-19.expected.json +18 -0
  189. package/conformance/sheet/sheet-hostile-19.html +6 -0
  190. package/conformance/sheet/sheet-hostile-19.meta +5 -0
  191. package/conformance/sheet/sheet-hostile-20.expected.json +16 -0
  192. package/conformance/sheet/sheet-hostile-20.html +6 -0
  193. package/conformance/sheet/sheet-hostile-20.meta +5 -0
  194. package/conformance/sheet/sheet-hostile-21.expected.json +18 -0
  195. package/conformance/sheet/sheet-hostile-21.html +6 -0
  196. package/conformance/sheet/sheet-hostile-21.meta +5 -0
  197. package/conformance/sheet/sheet-hostile-22.expected.json +21 -0
  198. package/conformance/sheet/sheet-hostile-22.html +6 -0
  199. package/conformance/sheet/sheet-hostile-22.meta +5 -0
  200. package/conformance/sheet/sheet-hostile-23.expected.json +18 -0
  201. package/conformance/sheet/sheet-hostile-23.html +6 -0
  202. package/conformance/sheet/sheet-hostile-23.meta +5 -0
  203. package/conformance/sheet/sheet-hostile-24.expected.json +18 -0
  204. package/conformance/sheet/sheet-hostile-24.html +6 -0
  205. package/conformance/sheet/sheet-hostile-24.meta +5 -0
  206. package/conformance/sheet/sheet-hostile-25.expected.json +18 -0
  207. package/conformance/sheet/sheet-hostile-25.html +6 -0
  208. package/conformance/sheet/sheet-hostile-25.meta +5 -0
  209. package/conformance/sheet/sheet-hostile-26.expected.json +18 -0
  210. package/conformance/sheet/sheet-hostile-26.html +6 -0
  211. package/conformance/sheet/sheet-hostile-26.meta +5 -0
  212. package/conformance/sheet/sheet-hostile-27.expected.json +24 -0
  213. package/conformance/sheet/sheet-hostile-27.html +7 -0
  214. package/conformance/sheet/sheet-hostile-27.meta +5 -0
  215. package/conformance/sheet/sheet-hostile-28.expected.json +18 -0
  216. package/conformance/sheet/sheet-hostile-28.html +6 -0
  217. package/conformance/sheet/sheet-hostile-28.meta +5 -0
  218. package/conformance/sheet/sheet-hostile-29.expected.json +18 -0
  219. package/conformance/sheet/sheet-hostile-29.html +6 -0
  220. package/conformance/sheet/sheet-hostile-29.meta +5 -0
  221. package/conformance/sheet/sheet-hostile-30.expected.json +18 -0
  222. package/conformance/sheet/sheet-hostile-30.html +6 -0
  223. package/conformance/sheet/sheet-hostile-30.meta +5 -0
  224. package/conformance/sheet/sheet-hostile-31.expected.json +19 -0
  225. package/conformance/sheet/sheet-hostile-31.html +6 -0
  226. package/conformance/sheet/sheet-hostile-31.meta +5 -0
  227. package/conformance/sheet/sheet-hostile-32.expected.json +24 -0
  228. package/conformance/sheet/sheet-hostile-32.html +6 -0
  229. package/conformance/sheet/sheet-hostile-32.meta +5 -0
  230. package/conformance/sheet/sheet-hostile-33.expected.json +18 -0
  231. package/conformance/sheet/sheet-hostile-33.html +6 -0
  232. package/conformance/sheet/sheet-hostile-33.meta +5 -0
  233. package/conformance/sheet/sheet-hostile-34.expected.json +24 -0
  234. package/conformance/sheet/sheet-hostile-34.html +8 -0
  235. package/conformance/sheet/sheet-hostile-34.meta +5 -0
  236. package/conformance/sheet/sheet-hostile-35.expected.json +24 -0
  237. package/conformance/sheet/sheet-hostile-35.html +8 -0
  238. package/conformance/sheet/sheet-hostile-35.meta +5 -0
  239. package/conformance/sheet/sheet-patch-append.after.html +27 -0
  240. package/conformance/sheet/sheet-patch-append.after.json +143 -0
  241. package/conformance/sheet/sheet-patch-append.data.json +15 -0
  242. package/conformance/sheet/sheet-patch-append.html +25 -0
  243. package/conformance/sheet/sheet-patch-append.meta +5 -0
  244. package/conformance/sheet/sheet-patch-append.write.json +7 -0
  245. package/conformance/sheet/sheet-patch-bad-range.data.json +11 -0
  246. package/conformance/sheet/sheet-patch-bad-range.error.json +6 -0
  247. package/conformance/sheet/sheet-patch-bad-range.html +25 -0
  248. package/conformance/sheet/sheet-patch-bad-range.meta +5 -0
  249. package/conformance/sheet/sheet-patch-bad-value.data.json +13 -0
  250. package/conformance/sheet/sheet-patch-bad-value.error.json +6 -0
  251. package/conformance/sheet/sheet-patch-bad-value.html +25 -0
  252. package/conformance/sheet/sheet-patch-bad-value.meta +5 -0
  253. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.html +25 -0
  254. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.json +129 -0
  255. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.data.json +11 -0
  256. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.html +25 -0
  257. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.meta +5 -0
  258. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.write.json +7 -0
  259. package/conformance/sheet/sheet-patch-covered-cell.data.json +10 -0
  260. package/conformance/sheet/sheet-patch-covered-cell.error.json +6 -0
  261. package/conformance/sheet/sheet-patch-covered-cell.html +10 -0
  262. package/conformance/sheet/sheet-patch-covered-cell.meta +5 -0
  263. package/conformance/sheet/sheet-patch-empty.data.json +1 -0
  264. package/conformance/sheet/sheet-patch-empty.error.json +6 -0
  265. package/conformance/sheet/sheet-patch-empty.html +25 -0
  266. package/conformance/sheet/sheet-patch-empty.meta +5 -0
  267. package/conformance/sheet/sheet-patch-fallback-route.after.html +24 -0
  268. package/conformance/sheet/sheet-patch-fallback-route.after.json +86 -0
  269. package/conformance/sheet/sheet-patch-fallback-route.data.json +10 -0
  270. package/conformance/sheet/sheet-patch-fallback-route.html +24 -0
  271. package/conformance/sheet/sheet-patch-fallback-route.meta +5 -0
  272. package/conformance/sheet/sheet-patch-fallback-route.write.json +7 -0
  273. package/conformance/sheet/sheet-patch-first-tab.after.html +25 -0
  274. package/conformance/sheet/sheet-patch-first-tab.after.json +129 -0
  275. package/conformance/sheet/sheet-patch-first-tab.data.json +10 -0
  276. package/conformance/sheet/sheet-patch-first-tab.html +25 -0
  277. package/conformance/sheet/sheet-patch-first-tab.meta +5 -0
  278. package/conformance/sheet/sheet-patch-first-tab.write.json +7 -0
  279. package/conformance/sheet/sheet-patch-formula.after.html +25 -0
  280. package/conformance/sheet/sheet-patch-formula.after.json +128 -0
  281. package/conformance/sheet/sheet-patch-formula.data.json +11 -0
  282. package/conformance/sheet/sheet-patch-formula.html +25 -0
  283. package/conformance/sheet/sheet-patch-formula.meta +5 -0
  284. package/conformance/sheet/sheet-patch-formula.write.json +7 -0
  285. package/conformance/sheet/sheet-patch-no-sheet-rule.data.json +10 -0
  286. package/conformance/sheet/sheet-patch-no-sheet-rule.error.json +6 -0
  287. package/conformance/sheet/sheet-patch-no-sheet-rule.html +18 -0
  288. package/conformance/sheet/sheet-patch-no-sheet-rule.meta +5 -0
  289. package/conformance/sheet/sheet-patch-no-values.data.json +6 -0
  290. package/conformance/sheet/sheet-patch-no-values.error.json +6 -0
  291. package/conformance/sheet/sheet-patch-no-values.html +25 -0
  292. package/conformance/sheet/sheet-patch-no-values.meta +5 -0
  293. package/conformance/sheet/sheet-patch-not-canonical.data.json +11 -0
  294. package/conformance/sheet/sheet-patch-not-canonical.error.json +6 -0
  295. package/conformance/sheet/sheet-patch-not-canonical.html +25 -0
  296. package/conformance/sheet/sheet-patch-not-canonical.meta +5 -0
  297. package/conformance/sheet/sheet-patch-outside-sheet.data.json +11 -0
  298. package/conformance/sheet/sheet-patch-outside-sheet.error.json +6 -0
  299. package/conformance/sheet/sheet-patch-outside-sheet.html +25 -0
  300. package/conformance/sheet/sheet-patch-outside-sheet.meta +5 -0
  301. package/conformance/sheet/sheet-patch-range.after.html +25 -0
  302. package/conformance/sheet/sheet-patch-range.after.json +126 -0
  303. package/conformance/sheet/sheet-patch-range.data.json +11 -0
  304. package/conformance/sheet/sheet-patch-range.html +25 -0
  305. package/conformance/sheet/sheet-patch-range.meta +5 -0
  306. package/conformance/sheet/sheet-patch-range.write.json +7 -0
  307. package/conformance/sheet/sheet-patch-two-tabs.after.html +25 -0
  308. package/conformance/sheet/sheet-patch-two-tabs.after.json +130 -0
  309. package/conformance/sheet/sheet-patch-two-tabs.data.json +20 -0
  310. package/conformance/sheet/sheet-patch-two-tabs.html +25 -0
  311. package/conformance/sheet/sheet-patch-two-tabs.meta +5 -0
  312. package/conformance/sheet/sheet-patch-two-tabs.write.json +7 -0
  313. package/conformance/sheet/sheet-patch-typed.after.html +25 -0
  314. package/conformance/sheet/sheet-patch-typed.after.json +121 -0
  315. package/conformance/sheet/sheet-patch-typed.data.json +13 -0
  316. package/conformance/sheet/sheet-patch-typed.html +25 -0
  317. package/conformance/sheet/sheet-patch-typed.meta +5 -0
  318. package/conformance/sheet/sheet-patch-typed.write.json +7 -0
  319. package/conformance/sheet/sheet-patch-unknown-sheet.data.json +11 -0
  320. package/conformance/sheet/sheet-patch-unknown-sheet.error.json +6 -0
  321. package/conformance/sheet/sheet-patch-unknown-sheet.html +25 -0
  322. package/conformance/sheet/sheet-patch-unknown-sheet.meta +5 -0
  323. package/conformance/sheet/sheet-read-bad-range.error.json +6 -0
  324. package/conformance/sheet/sheet-read-bad-range.html +25 -0
  325. package/conformance/sheet/sheet-read-bad-range.meta +6 -0
  326. package/conformance/sheet/sheet-read-bad-render.error.json +6 -0
  327. package/conformance/sheet/sheet-read-bad-render.html +25 -0
  328. package/conformance/sheet/sheet-read-bad-render.meta +6 -0
  329. package/conformance/sheet/sheet-read-digest.expected.json +91 -0
  330. package/conformance/sheet/sheet-read-digest.html +25 -0
  331. package/conformance/sheet/sheet-read-digest.meta +6 -0
  332. package/conformance/sheet/sheet-read-empty.expected.json +11 -0
  333. package/conformance/sheet/sheet-read-empty.html +6 -0
  334. package/conformance/sheet/sheet-read-empty.meta +5 -0
  335. package/conformance/sheet/sheet-read-escapes.expected.json +108 -0
  336. package/conformance/sheet/sheet-read-escapes.html +22 -0
  337. package/conformance/sheet/sheet-read-escapes.meta +5 -0
  338. package/conformance/sheet/sheet-read-formulas-formatted.expected.json +298 -0
  339. package/conformance/sheet/sheet-read-formulas-formatted.html +16 -0
  340. package/conformance/sheet/sheet-read-formulas-formatted.meta +5 -0
  341. package/conformance/sheet/sheet-read-grid-in-comment.expected.json +85 -0
  342. package/conformance/sheet/sheet-read-grid-in-comment.html +24 -0
  343. package/conformance/sheet/sheet-read-grid-in-comment.meta +5 -0
  344. package/conformance/sheet/sheet-read-grid-in-script.expected.json +85 -0
  345. package/conformance/sheet/sheet-read-grid-in-script.html +24 -0
  346. package/conformance/sheet/sheet-read-grid-in-script.meta +5 -0
  347. package/conformance/sheet/sheet-read-grid-in-template.expected.json +85 -0
  348. package/conformance/sheet/sheet-read-grid-in-template.html +24 -0
  349. package/conformance/sheet/sheet-read-grid-in-template.meta +5 -0
  350. package/conformance/sheet/sheet-read-grid-in-textarea.expected.json +85 -0
  351. package/conformance/sheet/sheet-read-grid-in-textarea.html +24 -0
  352. package/conformance/sheet/sheet-read-grid-in-textarea.meta +5 -0
  353. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.expected.json +94 -0
  354. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.html +24 -0
  355. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.meta +5 -0
  356. package/conformance/sheet/sheet-read-grid-outside-sheet.expected.json +85 -0
  357. package/conformance/sheet/sheet-read-grid-outside-sheet.html +24 -0
  358. package/conformance/sheet/sheet-read-grid-outside-sheet.meta +5 -0
  359. package/conformance/sheet/sheet-read-hand-edited.expected.json +85 -0
  360. package/conformance/sheet/sheet-read-hand-edited.html +18 -0
  361. package/conformance/sheet/sheet-read-hand-edited.meta +5 -0
  362. package/conformance/sheet/sheet-read-index.expected.json +19 -0
  363. package/conformance/sheet/sheet-read-index.html +25 -0
  364. package/conformance/sheet/sheet-read-index.meta +6 -0
  365. package/conformance/sheet/sheet-read-merged.expected.json +59 -0
  366. package/conformance/sheet/sheet-read-merged.html +10 -0
  367. package/conformance/sheet/sheet-read-merged.meta +5 -0
  368. package/conformance/sheet/sheet-read-no-tag.error.json +5 -0
  369. package/conformance/sheet/sheet-read-no-tag.html +18 -0
  370. package/conformance/sheet/sheet-read-no-tag.meta +5 -0
  371. package/conformance/sheet/sheet-read-padded-name-index.expected.json +19 -0
  372. package/conformance/sheet/sheet-read-padded-name-index.html +25 -0
  373. package/conformance/sheet/sheet-read-padded-name-index.meta +6 -0
  374. package/conformance/sheet/sheet-read-padded-name-lookup.expected.json +83 -0
  375. package/conformance/sheet/sheet-read-padded-name-lookup.html +25 -0
  376. package/conformance/sheet/sheet-read-padded-name-lookup.meta +6 -0
  377. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.error.json +6 -0
  378. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.html +25 -0
  379. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.meta +6 -0
  380. package/conformance/sheet/sheet-read-padded-name.expected.json +128 -0
  381. package/conformance/sheet/sheet-read-padded-name.html +25 -0
  382. package/conformance/sheet/sheet-read-padded-name.meta +5 -0
  383. package/conformance/sheet/sheet-read-range-cells.expected.json +55 -0
  384. package/conformance/sheet/sheet-read-range-cells.html +25 -0
  385. package/conformance/sheet/sheet-read-range-cells.meta +6 -0
  386. package/conformance/sheet/sheet-read-range-first-tab.expected.json +12 -0
  387. package/conformance/sheet/sheet-read-range-first-tab.html +25 -0
  388. package/conformance/sheet/sheet-read-range-first-tab.meta +6 -0
  389. package/conformance/sheet/sheet-read-range-formulas.expected.json +24 -0
  390. package/conformance/sheet/sheet-read-range-formulas.html +25 -0
  391. package/conformance/sheet/sheet-read-range-formulas.meta +6 -0
  392. package/conformance/sheet/sheet-read-range-values.expected.json +24 -0
  393. package/conformance/sheet/sheet-read-range-values.html +25 -0
  394. package/conformance/sheet/sheet-read-range-values.meta +6 -0
  395. package/conformance/sheet/sheet-read-sheet-by-id.expected.json +46 -0
  396. package/conformance/sheet/sheet-read-sheet-by-id.html +25 -0
  397. package/conformance/sheet/sheet-read-sheet-by-id.meta +6 -0
  398. package/conformance/sheet/sheet-read-sheet-by-name.expected.json +46 -0
  399. package/conformance/sheet/sheet-read-sheet-by-name.html +25 -0
  400. package/conformance/sheet/sheet-read-sheet-by-name.meta +6 -0
  401. package/conformance/sheet/sheet-read-styled.expected.json +48 -0
  402. package/conformance/sheet/sheet-read-styled.html +10 -0
  403. package/conformance/sheet/sheet-read-styled.meta +5 -0
  404. package/conformance/sheet/sheet-read-tiny.expected.json +85 -0
  405. package/conformance/sheet/sheet-read-tiny.html +18 -0
  406. package/conformance/sheet/sheet-read-tiny.meta +5 -0
  407. package/conformance/sheet/sheet-read-two-tabs.expected.json +128 -0
  408. package/conformance/sheet/sheet-read-two-tabs.html +25 -0
  409. package/conformance/sheet/sheet-read-two-tabs.meta +5 -0
  410. package/conformance/sheet/sheet-read-unknown-sheet.error.json +6 -0
  411. package/conformance/sheet/sheet-read-unknown-sheet.html +25 -0
  412. package/conformance/sheet/sheet-read-unknown-sheet.meta +6 -0
  413. package/conformance/sheet/sheet-read-version-1-codec.error.json +5 -0
  414. package/conformance/sheet/sheet-read-version-1-codec.html +25 -0
  415. package/conformance/sheet/sheet-read-version-1-codec.meta +5 -0
  416. package/conformance/sheet/sheet-write-whole-refused.data.json +3 -0
  417. package/conformance/sheet/sheet-write-whole-refused.error.json +7 -0
  418. package/conformance/sheet/sheet-write-whole-refused.html +25 -0
  419. package/conformance/sheet/sheet-write-whole-refused.meta +5 -0
  420. package/conformance/sheet-guard-extraction.json +616 -0
  421. package/conformance/sheet-guard.json +32362 -0
  422. package/conformance/sheet-read-shapes.json +1 -0
  423. package/dist/hyper-html-api.engine.esm.js +1 -1
  424. package/dist/hyper-html-api.engine.min.js +1 -1
  425. package/dist/hyper-html-api.min.js +8 -8
  426. package/dist/hyper-html-api.upgrade.min.js +2 -2
  427. package/package.json +6 -3
  428. package/src/codecs/sheet.js +127 -0
  429. package/src/codecs/sheet.vendor.js +887 -0
  430. package/src/dom-api.js +20 -11
  431. package/src/engine/apply.js +17 -7
  432. package/src/engine/codecs.js +80 -0
  433. package/src/engine/errors.js +20 -1
  434. package/src/engine/extract.js +17 -3
  435. package/src/engine/index.js +9 -5
  436. package/src/engine/round-trip.js +24 -10
  437. package/src/engine/rule-syntax.js +29 -3
  438. package/src/engine/rules-tag.js +3 -2
  439. package/src/engine/write-plan.js +4 -3
  440. package/src/page.js +15 -5
  441. package/src/patch-document.js +122 -0
  442. package/src/read.js +163 -0
  443. package/src/site.js +164 -0
  444. package/src/upgrade/codec-carry.js +23 -0
  445. package/src/upgrade/extract-all.js +12 -3
  446. package/src/upgrade/migrate.js +31 -3
  447. package/src/upgrade/rules-tags.js +4 -4
  448. package/src/upgrade/shape-match.js +32 -13
  449. 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
- dataByName[name] = extract(adapter, root, rules)
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
  }
@@ -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 { data, summary } = shapeMatch(dataByName[name], rules)
24
- apply(adapter, root, rules, data)
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 { SUPPORTED_VERSION } from '../engine/rules-tag.js'
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 !== SUPPORTED_VERSION) throw new UnknownRulesVersion(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
  }