hyper-html-api 0.14.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 (415) hide show
  1. package/conformance/MANIFEST.json +2 -2
  2. package/conformance/cases/codec-form-nested.error.json +5 -0
  3. package/conformance/cases/codec-form-nested.html +4 -0
  4. package/conformance/cases/codec-form-nested.meta +6 -0
  5. package/conformance/cases/codec-form-nested.parsed.json +5 -0
  6. package/conformance/cases/codec-form-star.error.json +5 -0
  7. package/conformance/cases/codec-form-star.html +4 -0
  8. package/conformance/cases/codec-form-star.meta +6 -0
  9. package/conformance/cases/codec-form-star.parsed.json +3 -0
  10. package/conformance/cases/codec-form-under-v1.error.json +5 -0
  11. package/conformance/cases/codec-form-under-v1.html +4 -0
  12. package/conformance/cases/codec-form-under-v1.meta +6 -0
  13. package/conformance/cases/codec-form-under-v1.parsed.json +3 -0
  14. package/conformance/cases/codec-unknown-name.error.json +5 -0
  15. package/conformance/cases/codec-unknown-name.html +4 -0
  16. package/conformance/cases/codec-unknown-name.meta +6 -0
  17. package/conformance/cases/codec-unknown-name.parsed.json +3 -0
  18. package/conformance/cases/tag-version-2-plain.expected.json +7 -0
  19. package/conformance/cases/tag-version-2-plain.html +5 -0
  20. package/conformance/cases/tag-version-2-plain.meta +6 -0
  21. package/conformance/cases/tag-version-2-plain.parsed.json +4 -0
  22. package/conformance/cases/tag-wrong-version.error.json +1 -1
  23. package/conformance/cases/tag-wrong-version.html +1 -1
  24. package/conformance/sheet/body-data-write.body.json +3 -0
  25. package/conformance/sheet/body-data-write.data.json +3 -0
  26. package/conformance/sheet/body-data-write.html +25 -0
  27. package/conformance/sheet/body-data-write.meta +5 -0
  28. package/conformance/sheet/body-patch-key-in-rules.body.json +3 -0
  29. package/conformance/sheet/body-patch-key-in-rules.data.json +13 -0
  30. package/conformance/sheet/body-patch-key-in-rules.html +18 -0
  31. package/conformance/sheet/body-patch-key-in-rules.meta +5 -0
  32. package/conformance/sheet/body-patch-not-array.body.json +3 -0
  33. package/conformance/sheet/body-patch-not-array.data.json +3 -0
  34. package/conformance/sheet/body-patch-not-array.html +25 -0
  35. package/conformance/sheet/body-patch-not-array.meta +5 -0
  36. package/conformance/sheet/body-patch-on-sheet.body.json +3 -0
  37. package/conformance/sheet/body-patch-on-sheet.data.json +14 -0
  38. package/conformance/sheet/body-patch-on-sheet.html +25 -0
  39. package/conformance/sheet/body-patch-on-sheet.meta +5 -0
  40. package/conformance/sheet/body-patch-version-1.body.json +3 -0
  41. package/conformance/sheet/body-patch-version-1.data.json +13 -0
  42. package/conformance/sheet/body-patch-version-1.html +4 -0
  43. package/conformance/sheet/body-patch-version-1.meta +5 -0
  44. package/conformance/sheet/body-patch-with-other-key.body.json +3 -0
  45. package/conformance/sheet/body-patch-with-other-key.data.json +14 -0
  46. package/conformance/sheet/body-patch-with-other-key.html +25 -0
  47. package/conformance/sheet/body-patch-with-other-key.meta +5 -0
  48. package/conformance/sheet/body-patch-without-operation-id.body.json +3 -0
  49. package/conformance/sheet/body-patch-without-operation-id.data.json +13 -0
  50. package/conformance/sheet/body-patch-without-operation-id.html +25 -0
  51. package/conformance/sheet/body-patch-without-operation-id.meta +5 -0
  52. package/conformance/sheet/params-bad-range.error.json +6 -0
  53. package/conformance/sheet/params-bad-range.html +25 -0
  54. package/conformance/sheet/params-bad-range.meta +6 -0
  55. package/conformance/sheet/params-bad-render.error.json +6 -0
  56. package/conformance/sheet/params-bad-render.html +25 -0
  57. package/conformance/sheet/params-bad-render.meta +6 -0
  58. package/conformance/sheet/params-bad-shape.error.json +6 -0
  59. package/conformance/sheet/params-bad-shape.html +25 -0
  60. package/conformance/sheet/params-bad-shape.meta +6 -0
  61. package/conformance/sheet/params-cache-buster.expected.json +128 -0
  62. package/conformance/sheet/params-cache-buster.html +25 -0
  63. package/conformance/sheet/params-cache-buster.meta +6 -0
  64. package/conformance/sheet/params-data-alone.expected.json +128 -0
  65. package/conformance/sheet/params-data-alone.html +25 -0
  66. package/conformance/sheet/params-data-alone.meta +6 -0
  67. package/conformance/sheet/params-empty.error.json +6 -0
  68. package/conformance/sheet/params-empty.html +25 -0
  69. package/conformance/sheet/params-empty.meta +6 -0
  70. package/conformance/sheet/params-index.expected.json +19 -0
  71. package/conformance/sheet/params-index.html +25 -0
  72. package/conformance/sheet/params-index.meta +6 -0
  73. package/conformance/sheet/params-none.expected.json +128 -0
  74. package/conformance/sheet/params-none.html +25 -0
  75. package/conformance/sheet/params-none.meta +6 -0
  76. package/conformance/sheet/params-range.expected.json +17 -0
  77. package/conformance/sheet/params-range.html +25 -0
  78. package/conformance/sheet/params-range.meta +6 -0
  79. package/conformance/sheet/params-render-without-range.error.json +6 -0
  80. package/conformance/sheet/params-render-without-range.html +25 -0
  81. package/conformance/sheet/params-render-without-range.meta +6 -0
  82. package/conformance/sheet/params-repeated.error.json +6 -0
  83. package/conformance/sheet/params-repeated.html +25 -0
  84. package/conformance/sheet/params-repeated.meta +6 -0
  85. package/conformance/sheet/params-unknown-sheet.error.json +6 -0
  86. package/conformance/sheet/params-unknown-sheet.html +25 -0
  87. package/conformance/sheet/params-unknown-sheet.meta +6 -0
  88. package/conformance/sheet/params-with-data.error.json +6 -0
  89. package/conformance/sheet/params-with-data.html +25 -0
  90. package/conformance/sheet/params-with-data.meta +6 -0
  91. package/conformance/sheet/sheet-contract-cells.expected.json +57 -0
  92. package/conformance/sheet/sheet-contract-cells.html +10 -0
  93. package/conformance/sheet/sheet-contract-cells.meta +6 -0
  94. package/conformance/sheet/sheet-contract-formulas.expected.json +29 -0
  95. package/conformance/sheet/sheet-contract-formulas.html +10 -0
  96. package/conformance/sheet/sheet-contract-formulas.meta +6 -0
  97. package/conformance/sheet/sheet-contract-values.expected.json +29 -0
  98. package/conformance/sheet/sheet-contract-values.html +10 -0
  99. package/conformance/sheet/sheet-contract-values.meta +6 -0
  100. package/conformance/sheet/sheet-contract-whole.expected.json +59 -0
  101. package/conformance/sheet/sheet-contract-whole.html +10 -0
  102. package/conformance/sheet/sheet-contract-whole.meta +5 -0
  103. package/conformance/sheet/sheet-hostile-01.expected.json +18 -0
  104. package/conformance/sheet/sheet-hostile-01.html +6 -0
  105. package/conformance/sheet/sheet-hostile-01.meta +5 -0
  106. package/conformance/sheet/sheet-hostile-02.expected.json +18 -0
  107. package/conformance/sheet/sheet-hostile-02.html +6 -0
  108. package/conformance/sheet/sheet-hostile-02.meta +5 -0
  109. package/conformance/sheet/sheet-hostile-03.expected.json +18 -0
  110. package/conformance/sheet/sheet-hostile-03.html +6 -0
  111. package/conformance/sheet/sheet-hostile-03.meta +5 -0
  112. package/conformance/sheet/sheet-hostile-04.expected.json +18 -0
  113. package/conformance/sheet/sheet-hostile-04.html +6 -0
  114. package/conformance/sheet/sheet-hostile-04.meta +5 -0
  115. package/conformance/sheet/sheet-hostile-05.expected.json +18 -0
  116. package/conformance/sheet/sheet-hostile-05.html +6 -0
  117. package/conformance/sheet/sheet-hostile-05.meta +5 -0
  118. package/conformance/sheet/sheet-hostile-06.expected.json +21 -0
  119. package/conformance/sheet/sheet-hostile-06.html +6 -0
  120. package/conformance/sheet/sheet-hostile-06.meta +5 -0
  121. package/conformance/sheet/sheet-hostile-07.expected.json +21 -0
  122. package/conformance/sheet/sheet-hostile-07.html +6 -0
  123. package/conformance/sheet/sheet-hostile-07.meta +5 -0
  124. package/conformance/sheet/sheet-hostile-08.expected.json +21 -0
  125. package/conformance/sheet/sheet-hostile-08.html +6 -0
  126. package/conformance/sheet/sheet-hostile-08.meta +5 -0
  127. package/conformance/sheet/sheet-hostile-09.expected.json +21 -0
  128. package/conformance/sheet/sheet-hostile-09.html +6 -0
  129. package/conformance/sheet/sheet-hostile-09.meta +5 -0
  130. package/conformance/sheet/sheet-hostile-10.expected.json +21 -0
  131. package/conformance/sheet/sheet-hostile-10.html +6 -0
  132. package/conformance/sheet/sheet-hostile-10.meta +5 -0
  133. package/conformance/sheet/sheet-hostile-11.expected.json +22 -0
  134. package/conformance/sheet/sheet-hostile-11.html +6 -0
  135. package/conformance/sheet/sheet-hostile-11.meta +5 -0
  136. package/conformance/sheet/sheet-hostile-12.expected.json +18 -0
  137. package/conformance/sheet/sheet-hostile-12.html +6 -0
  138. package/conformance/sheet/sheet-hostile-12.meta +5 -0
  139. package/conformance/sheet/sheet-hostile-13.expected.json +18 -0
  140. package/conformance/sheet/sheet-hostile-13.html +6 -0
  141. package/conformance/sheet/sheet-hostile-13.meta +5 -0
  142. package/conformance/sheet/sheet-hostile-14.expected.json +21 -0
  143. package/conformance/sheet/sheet-hostile-14.html +6 -0
  144. package/conformance/sheet/sheet-hostile-14.meta +5 -0
  145. package/conformance/sheet/sheet-hostile-15.expected.json +18 -0
  146. package/conformance/sheet/sheet-hostile-15.html +0 -0
  147. package/conformance/sheet/sheet-hostile-15.meta +5 -0
  148. package/conformance/sheet/sheet-hostile-16.expected.json +18 -0
  149. package/conformance/sheet/sheet-hostile-16.html +6 -0
  150. package/conformance/sheet/sheet-hostile-16.meta +5 -0
  151. package/conformance/sheet/sheet-hostile-17.expected.json +18 -0
  152. package/conformance/sheet/sheet-hostile-17.html +6 -0
  153. package/conformance/sheet/sheet-hostile-17.meta +5 -0
  154. package/conformance/sheet/sheet-hostile-18.expected.json +18 -0
  155. package/conformance/sheet/sheet-hostile-18.html +6 -0
  156. package/conformance/sheet/sheet-hostile-18.meta +5 -0
  157. package/conformance/sheet/sheet-hostile-19.expected.json +18 -0
  158. package/conformance/sheet/sheet-hostile-19.html +6 -0
  159. package/conformance/sheet/sheet-hostile-19.meta +5 -0
  160. package/conformance/sheet/sheet-hostile-20.expected.json +16 -0
  161. package/conformance/sheet/sheet-hostile-20.html +6 -0
  162. package/conformance/sheet/sheet-hostile-20.meta +5 -0
  163. package/conformance/sheet/sheet-hostile-21.expected.json +18 -0
  164. package/conformance/sheet/sheet-hostile-21.html +6 -0
  165. package/conformance/sheet/sheet-hostile-21.meta +5 -0
  166. package/conformance/sheet/sheet-hostile-22.expected.json +21 -0
  167. package/conformance/sheet/sheet-hostile-22.html +6 -0
  168. package/conformance/sheet/sheet-hostile-22.meta +5 -0
  169. package/conformance/sheet/sheet-hostile-23.expected.json +18 -0
  170. package/conformance/sheet/sheet-hostile-23.html +6 -0
  171. package/conformance/sheet/sheet-hostile-23.meta +5 -0
  172. package/conformance/sheet/sheet-hostile-24.expected.json +18 -0
  173. package/conformance/sheet/sheet-hostile-24.html +6 -0
  174. package/conformance/sheet/sheet-hostile-24.meta +5 -0
  175. package/conformance/sheet/sheet-hostile-25.expected.json +18 -0
  176. package/conformance/sheet/sheet-hostile-25.html +6 -0
  177. package/conformance/sheet/sheet-hostile-25.meta +5 -0
  178. package/conformance/sheet/sheet-hostile-26.expected.json +18 -0
  179. package/conformance/sheet/sheet-hostile-26.html +6 -0
  180. package/conformance/sheet/sheet-hostile-26.meta +5 -0
  181. package/conformance/sheet/sheet-hostile-27.expected.json +24 -0
  182. package/conformance/sheet/sheet-hostile-27.html +7 -0
  183. package/conformance/sheet/sheet-hostile-27.meta +5 -0
  184. package/conformance/sheet/sheet-hostile-28.expected.json +18 -0
  185. package/conformance/sheet/sheet-hostile-28.html +6 -0
  186. package/conformance/sheet/sheet-hostile-28.meta +5 -0
  187. package/conformance/sheet/sheet-hostile-29.expected.json +18 -0
  188. package/conformance/sheet/sheet-hostile-29.html +6 -0
  189. package/conformance/sheet/sheet-hostile-29.meta +5 -0
  190. package/conformance/sheet/sheet-hostile-30.expected.json +18 -0
  191. package/conformance/sheet/sheet-hostile-30.html +6 -0
  192. package/conformance/sheet/sheet-hostile-30.meta +5 -0
  193. package/conformance/sheet/sheet-hostile-31.expected.json +19 -0
  194. package/conformance/sheet/sheet-hostile-31.html +6 -0
  195. package/conformance/sheet/sheet-hostile-31.meta +5 -0
  196. package/conformance/sheet/sheet-hostile-32.expected.json +24 -0
  197. package/conformance/sheet/sheet-hostile-32.html +6 -0
  198. package/conformance/sheet/sheet-hostile-32.meta +5 -0
  199. package/conformance/sheet/sheet-hostile-33.expected.json +18 -0
  200. package/conformance/sheet/sheet-hostile-33.html +6 -0
  201. package/conformance/sheet/sheet-hostile-33.meta +5 -0
  202. package/conformance/sheet/sheet-hostile-34.expected.json +24 -0
  203. package/conformance/sheet/sheet-hostile-34.html +8 -0
  204. package/conformance/sheet/sheet-hostile-34.meta +5 -0
  205. package/conformance/sheet/sheet-hostile-35.expected.json +24 -0
  206. package/conformance/sheet/sheet-hostile-35.html +8 -0
  207. package/conformance/sheet/sheet-hostile-35.meta +5 -0
  208. package/conformance/sheet/sheet-patch-append.after.html +27 -0
  209. package/conformance/sheet/sheet-patch-append.after.json +143 -0
  210. package/conformance/sheet/sheet-patch-append.data.json +15 -0
  211. package/conformance/sheet/sheet-patch-append.html +25 -0
  212. package/conformance/sheet/sheet-patch-append.meta +5 -0
  213. package/conformance/sheet/sheet-patch-append.write.json +7 -0
  214. package/conformance/sheet/sheet-patch-bad-range.data.json +11 -0
  215. package/conformance/sheet/sheet-patch-bad-range.error.json +6 -0
  216. package/conformance/sheet/sheet-patch-bad-range.html +25 -0
  217. package/conformance/sheet/sheet-patch-bad-range.meta +5 -0
  218. package/conformance/sheet/sheet-patch-bad-value.data.json +13 -0
  219. package/conformance/sheet/sheet-patch-bad-value.error.json +6 -0
  220. package/conformance/sheet/sheet-patch-bad-value.html +25 -0
  221. package/conformance/sheet/sheet-patch-bad-value.meta +5 -0
  222. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.html +25 -0
  223. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.after.json +129 -0
  224. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.data.json +11 -0
  225. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.html +25 -0
  226. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.meta +5 -0
  227. package/conformance/sheet/sheet-patch-clean-tab-beside-edited.write.json +7 -0
  228. package/conformance/sheet/sheet-patch-covered-cell.data.json +10 -0
  229. package/conformance/sheet/sheet-patch-covered-cell.error.json +6 -0
  230. package/conformance/sheet/sheet-patch-covered-cell.html +10 -0
  231. package/conformance/sheet/sheet-patch-covered-cell.meta +5 -0
  232. package/conformance/sheet/sheet-patch-empty.data.json +1 -0
  233. package/conformance/sheet/sheet-patch-empty.error.json +6 -0
  234. package/conformance/sheet/sheet-patch-empty.html +25 -0
  235. package/conformance/sheet/sheet-patch-empty.meta +5 -0
  236. package/conformance/sheet/sheet-patch-fallback-route.after.html +24 -0
  237. package/conformance/sheet/sheet-patch-fallback-route.after.json +86 -0
  238. package/conformance/sheet/sheet-patch-fallback-route.data.json +10 -0
  239. package/conformance/sheet/sheet-patch-fallback-route.html +24 -0
  240. package/conformance/sheet/sheet-patch-fallback-route.meta +5 -0
  241. package/conformance/sheet/sheet-patch-fallback-route.write.json +7 -0
  242. package/conformance/sheet/sheet-patch-first-tab.after.html +25 -0
  243. package/conformance/sheet/sheet-patch-first-tab.after.json +129 -0
  244. package/conformance/sheet/sheet-patch-first-tab.data.json +10 -0
  245. package/conformance/sheet/sheet-patch-first-tab.html +25 -0
  246. package/conformance/sheet/sheet-patch-first-tab.meta +5 -0
  247. package/conformance/sheet/sheet-patch-first-tab.write.json +7 -0
  248. package/conformance/sheet/sheet-patch-formula.after.html +25 -0
  249. package/conformance/sheet/sheet-patch-formula.after.json +128 -0
  250. package/conformance/sheet/sheet-patch-formula.data.json +11 -0
  251. package/conformance/sheet/sheet-patch-formula.html +25 -0
  252. package/conformance/sheet/sheet-patch-formula.meta +5 -0
  253. package/conformance/sheet/sheet-patch-formula.write.json +7 -0
  254. package/conformance/sheet/sheet-patch-no-sheet-rule.data.json +10 -0
  255. package/conformance/sheet/sheet-patch-no-sheet-rule.error.json +6 -0
  256. package/conformance/sheet/sheet-patch-no-sheet-rule.html +18 -0
  257. package/conformance/sheet/sheet-patch-no-sheet-rule.meta +5 -0
  258. package/conformance/sheet/sheet-patch-no-values.data.json +6 -0
  259. package/conformance/sheet/sheet-patch-no-values.error.json +6 -0
  260. package/conformance/sheet/sheet-patch-no-values.html +25 -0
  261. package/conformance/sheet/sheet-patch-no-values.meta +5 -0
  262. package/conformance/sheet/sheet-patch-not-canonical.data.json +11 -0
  263. package/conformance/sheet/sheet-patch-not-canonical.error.json +6 -0
  264. package/conformance/sheet/sheet-patch-not-canonical.html +25 -0
  265. package/conformance/sheet/sheet-patch-not-canonical.meta +5 -0
  266. package/conformance/sheet/sheet-patch-outside-sheet.data.json +11 -0
  267. package/conformance/sheet/sheet-patch-outside-sheet.error.json +6 -0
  268. package/conformance/sheet/sheet-patch-outside-sheet.html +25 -0
  269. package/conformance/sheet/sheet-patch-outside-sheet.meta +5 -0
  270. package/conformance/sheet/sheet-patch-range.after.html +25 -0
  271. package/conformance/sheet/sheet-patch-range.after.json +126 -0
  272. package/conformance/sheet/sheet-patch-range.data.json +11 -0
  273. package/conformance/sheet/sheet-patch-range.html +25 -0
  274. package/conformance/sheet/sheet-patch-range.meta +5 -0
  275. package/conformance/sheet/sheet-patch-range.write.json +7 -0
  276. package/conformance/sheet/sheet-patch-two-tabs.after.html +25 -0
  277. package/conformance/sheet/sheet-patch-two-tabs.after.json +130 -0
  278. package/conformance/sheet/sheet-patch-two-tabs.data.json +20 -0
  279. package/conformance/sheet/sheet-patch-two-tabs.html +25 -0
  280. package/conformance/sheet/sheet-patch-two-tabs.meta +5 -0
  281. package/conformance/sheet/sheet-patch-two-tabs.write.json +7 -0
  282. package/conformance/sheet/sheet-patch-typed.after.html +25 -0
  283. package/conformance/sheet/sheet-patch-typed.after.json +121 -0
  284. package/conformance/sheet/sheet-patch-typed.data.json +13 -0
  285. package/conformance/sheet/sheet-patch-typed.html +25 -0
  286. package/conformance/sheet/sheet-patch-typed.meta +5 -0
  287. package/conformance/sheet/sheet-patch-typed.write.json +7 -0
  288. package/conformance/sheet/sheet-patch-unknown-sheet.data.json +11 -0
  289. package/conformance/sheet/sheet-patch-unknown-sheet.error.json +6 -0
  290. package/conformance/sheet/sheet-patch-unknown-sheet.html +25 -0
  291. package/conformance/sheet/sheet-patch-unknown-sheet.meta +5 -0
  292. package/conformance/sheet/sheet-read-bad-range.error.json +6 -0
  293. package/conformance/sheet/sheet-read-bad-range.html +25 -0
  294. package/conformance/sheet/sheet-read-bad-range.meta +6 -0
  295. package/conformance/sheet/sheet-read-bad-render.error.json +6 -0
  296. package/conformance/sheet/sheet-read-bad-render.html +25 -0
  297. package/conformance/sheet/sheet-read-bad-render.meta +6 -0
  298. package/conformance/sheet/sheet-read-digest.expected.json +91 -0
  299. package/conformance/sheet/sheet-read-digest.html +25 -0
  300. package/conformance/sheet/sheet-read-digest.meta +6 -0
  301. package/conformance/sheet/sheet-read-empty.expected.json +11 -0
  302. package/conformance/sheet/sheet-read-empty.html +6 -0
  303. package/conformance/sheet/sheet-read-empty.meta +5 -0
  304. package/conformance/sheet/sheet-read-escapes.expected.json +108 -0
  305. package/conformance/sheet/sheet-read-escapes.html +22 -0
  306. package/conformance/sheet/sheet-read-escapes.meta +5 -0
  307. package/conformance/sheet/sheet-read-formulas-formatted.expected.json +298 -0
  308. package/conformance/sheet/sheet-read-formulas-formatted.html +16 -0
  309. package/conformance/sheet/sheet-read-formulas-formatted.meta +5 -0
  310. package/conformance/sheet/sheet-read-grid-in-comment.expected.json +85 -0
  311. package/conformance/sheet/sheet-read-grid-in-comment.html +24 -0
  312. package/conformance/sheet/sheet-read-grid-in-comment.meta +5 -0
  313. package/conformance/sheet/sheet-read-grid-in-script.expected.json +85 -0
  314. package/conformance/sheet/sheet-read-grid-in-script.html +24 -0
  315. package/conformance/sheet/sheet-read-grid-in-script.meta +5 -0
  316. package/conformance/sheet/sheet-read-grid-in-template.expected.json +85 -0
  317. package/conformance/sheet/sheet-read-grid-in-template.html +24 -0
  318. package/conformance/sheet/sheet-read-grid-in-template.meta +5 -0
  319. package/conformance/sheet/sheet-read-grid-in-textarea.expected.json +85 -0
  320. package/conformance/sheet/sheet-read-grid-in-textarea.html +24 -0
  321. package/conformance/sheet/sheet-read-grid-in-textarea.meta +5 -0
  322. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.expected.json +94 -0
  323. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.html +24 -0
  324. package/conformance/sheet/sheet-read-grid-outside-read-by-rule.meta +5 -0
  325. package/conformance/sheet/sheet-read-grid-outside-sheet.expected.json +85 -0
  326. package/conformance/sheet/sheet-read-grid-outside-sheet.html +24 -0
  327. package/conformance/sheet/sheet-read-grid-outside-sheet.meta +5 -0
  328. package/conformance/sheet/sheet-read-hand-edited.expected.json +85 -0
  329. package/conformance/sheet/sheet-read-hand-edited.html +18 -0
  330. package/conformance/sheet/sheet-read-hand-edited.meta +5 -0
  331. package/conformance/sheet/sheet-read-index.expected.json +19 -0
  332. package/conformance/sheet/sheet-read-index.html +25 -0
  333. package/conformance/sheet/sheet-read-index.meta +6 -0
  334. package/conformance/sheet/sheet-read-merged.expected.json +59 -0
  335. package/conformance/sheet/sheet-read-merged.html +10 -0
  336. package/conformance/sheet/sheet-read-merged.meta +5 -0
  337. package/conformance/sheet/sheet-read-no-tag.error.json +5 -0
  338. package/conformance/sheet/sheet-read-no-tag.html +18 -0
  339. package/conformance/sheet/sheet-read-no-tag.meta +5 -0
  340. package/conformance/sheet/sheet-read-padded-name-index.expected.json +19 -0
  341. package/conformance/sheet/sheet-read-padded-name-index.html +25 -0
  342. package/conformance/sheet/sheet-read-padded-name-index.meta +6 -0
  343. package/conformance/sheet/sheet-read-padded-name-lookup.expected.json +83 -0
  344. package/conformance/sheet/sheet-read-padded-name-lookup.html +25 -0
  345. package/conformance/sheet/sheet-read-padded-name-lookup.meta +6 -0
  346. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.error.json +6 -0
  347. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.html +25 -0
  348. package/conformance/sheet/sheet-read-padded-name-trimmed-lookup.meta +6 -0
  349. package/conformance/sheet/sheet-read-padded-name.expected.json +128 -0
  350. package/conformance/sheet/sheet-read-padded-name.html +25 -0
  351. package/conformance/sheet/sheet-read-padded-name.meta +5 -0
  352. package/conformance/sheet/sheet-read-range-cells.expected.json +55 -0
  353. package/conformance/sheet/sheet-read-range-cells.html +25 -0
  354. package/conformance/sheet/sheet-read-range-cells.meta +6 -0
  355. package/conformance/sheet/sheet-read-range-first-tab.expected.json +12 -0
  356. package/conformance/sheet/sheet-read-range-first-tab.html +25 -0
  357. package/conformance/sheet/sheet-read-range-first-tab.meta +6 -0
  358. package/conformance/sheet/sheet-read-range-formulas.expected.json +24 -0
  359. package/conformance/sheet/sheet-read-range-formulas.html +25 -0
  360. package/conformance/sheet/sheet-read-range-formulas.meta +6 -0
  361. package/conformance/sheet/sheet-read-range-values.expected.json +24 -0
  362. package/conformance/sheet/sheet-read-range-values.html +25 -0
  363. package/conformance/sheet/sheet-read-range-values.meta +6 -0
  364. package/conformance/sheet/sheet-read-sheet-by-id.expected.json +46 -0
  365. package/conformance/sheet/sheet-read-sheet-by-id.html +25 -0
  366. package/conformance/sheet/sheet-read-sheet-by-id.meta +6 -0
  367. package/conformance/sheet/sheet-read-sheet-by-name.expected.json +46 -0
  368. package/conformance/sheet/sheet-read-sheet-by-name.html +25 -0
  369. package/conformance/sheet/sheet-read-sheet-by-name.meta +6 -0
  370. package/conformance/sheet/sheet-read-styled.expected.json +48 -0
  371. package/conformance/sheet/sheet-read-styled.html +10 -0
  372. package/conformance/sheet/sheet-read-styled.meta +5 -0
  373. package/conformance/sheet/sheet-read-tiny.expected.json +85 -0
  374. package/conformance/sheet/sheet-read-tiny.html +18 -0
  375. package/conformance/sheet/sheet-read-tiny.meta +5 -0
  376. package/conformance/sheet/sheet-read-two-tabs.expected.json +128 -0
  377. package/conformance/sheet/sheet-read-two-tabs.html +25 -0
  378. package/conformance/sheet/sheet-read-two-tabs.meta +5 -0
  379. package/conformance/sheet/sheet-read-unknown-sheet.error.json +6 -0
  380. package/conformance/sheet/sheet-read-unknown-sheet.html +25 -0
  381. package/conformance/sheet/sheet-read-unknown-sheet.meta +6 -0
  382. package/conformance/sheet/sheet-read-version-1-codec.error.json +5 -0
  383. package/conformance/sheet/sheet-read-version-1-codec.html +25 -0
  384. package/conformance/sheet/sheet-read-version-1-codec.meta +5 -0
  385. package/conformance/sheet/sheet-write-whole-refused.data.json +3 -0
  386. package/conformance/sheet/sheet-write-whole-refused.error.json +7 -0
  387. package/conformance/sheet/sheet-write-whole-refused.html +25 -0
  388. package/conformance/sheet/sheet-write-whole-refused.meta +5 -0
  389. package/conformance/sheet-guard-extraction.json +616 -0
  390. package/conformance/sheet-guard.json +32362 -0
  391. package/conformance/sheet-read-shapes.json +1 -0
  392. package/dist/hyper-html-api.engine.esm.js +1 -1
  393. package/dist/hyper-html-api.engine.min.js +1 -1
  394. package/dist/hyper-html-api.min.js +7 -7
  395. package/dist/hyper-html-api.upgrade.min.js +2 -2
  396. package/package.json +5 -3
  397. package/src/codecs/sheet.js +127 -0
  398. package/src/codecs/sheet.vendor.js +887 -0
  399. package/src/dom-api.js +20 -11
  400. package/src/engine/apply.js +12 -1
  401. package/src/engine/codecs.js +80 -0
  402. package/src/engine/errors.js +20 -1
  403. package/src/engine/extract.js +11 -0
  404. package/src/engine/index.js +9 -5
  405. package/src/engine/round-trip.js +16 -10
  406. package/src/engine/rule-syntax.js +16 -0
  407. package/src/engine/rules-tag.js +3 -2
  408. package/src/engine/write-plan.js +2 -0
  409. package/src/patch-document.js +122 -0
  410. package/src/read.js +163 -0
  411. package/src/upgrade/codec-carry.js +23 -0
  412. package/src/upgrade/extract-all.js +12 -3
  413. package/src/upgrade/migrate.js +30 -3
  414. package/src/upgrade/rules-tags.js +4 -4
  415. package/src/write.js +44 -5
package/src/dom-api.js CHANGED
@@ -4,9 +4,12 @@ import domAdapter, { createDomAdapter } from './adapters/dom.js'
4
4
  // The DOM-bound engine: every engine call pre-wired with the DOM adapter. Lives
5
5
  // here (instead of inline in hyper-html-api.js) so both the full CDN bundle and
6
6
  // the lean `data.js` entry share one definition.
7
+ // Calls default `codecs` to the ones registered with engine.codecs.register.
8
+ const withCodecs = (opts) => ({ codecs: engineMod.registeredCodecs(), ...opts })
9
+
7
10
  export const engine = {
8
- extract: (root, rules, opts = {}) => engineMod.extract(createDomAdapter(root, opts), root, rules, opts),
9
- apply: (root, rules, data, opts = {}) => engineMod.apply(createDomAdapter(root, { ...opts, _write: true }), root, rules, data, opts),
11
+ extract: (root, rules, opts = {}) => engineMod.extract(createDomAdapter(root, opts), root, rules, withCodecs(opts)),
12
+ apply: (root, rules, data, opts = {}) => engineMod.apply(createDomAdapter(root, { ...opts, _write: true }), root, rules, data, withCodecs(opts)),
10
13
  findRulesIn: (root, token) => engineMod.findRulesIn(domAdapter, root, token),
11
14
  findRules: (root, source) => engineMod.resolveRules(domAdapter, root, source),
12
15
  bind: (root, source, opts = {}) => {
@@ -15,12 +18,14 @@ export const engine = {
15
18
  const what = typeof source === 'string' ? `data-rules-name~="${source}"` : 'the provided rules object'
16
19
  throw new Error(`hyper-html-api: could not resolve rules for ${what}`)
17
20
  }
21
+ const callOpts = withCodecs({ version: found.version, ...opts })
18
22
  return {
19
23
  ...found,
20
- get: () => engineMod.extract(createDomAdapter(root, opts), root, found.rules, opts),
21
- set: data => engineMod.apply(createDomAdapter(root, { ...opts, _write: true }), root, found.rules, data, opts),
24
+ get: () => engineMod.extract(createDomAdapter(root, opts), root, found.rules, callOpts),
25
+ set: data => engineMod.apply(createDomAdapter(root, { ...opts, _write: true }), root, found.rules, data, callOpts),
22
26
  }
23
27
  },
28
+ codecs: { register: engineMod.registerCodec, get: engineMod.getCodec },
24
29
  parseStrict: engineMod.parseStrict,
25
30
  parseRelaxed: engineMod.parseRelaxed,
26
31
  // Adapter-free, like the parsers above: a consumer that has to split a rule
@@ -38,7 +43,7 @@ function defaultRules(root) {
38
43
  const searchRoot = root && root.ownerDocument ? root.ownerDocument : root
39
44
  const found = engineMod.findRulesIn(domAdapter, searchRoot, 'api')
40
45
  if (!found) throw new engineMod.errors.NoRulesTag('api')
41
- return found.rules
46
+ return { rules: found.rules, version: found.version }
42
47
  }
43
48
 
44
49
  function containingTree(root) {
@@ -119,8 +124,8 @@ function assertNoReadOnlyTargets(rule, value) {
119
124
  }
120
125
  }
121
126
 
122
- function guardedApply(root, rules, data) {
123
- const opts = { templateAttr: 'cms-template' }
127
+ function guardedApply(root, rules, data, version) {
128
+ const opts = withCodecs({ templateAttr: 'cms-template', version })
124
129
  const liveTree = containingTree(root)
125
130
  const rootPath = childIndexPath(liveTree, root)
126
131
  const detachedTree = cloneContainingTree(liveTree)
@@ -145,7 +150,8 @@ function guardedApply(root, rules, data) {
145
150
 
146
151
  function exposeWriteError(error) {
147
152
  if (!(error instanceof Error)) return error
148
- const code = error.name || 'Error'
153
+ const keepsCode = error instanceof engineMod.errors.UnknownCodec || error instanceof engineMod.errors.CodecRefused
154
+ const code = keepsCode ? error.code : error.name || 'Error'
149
155
  try {
150
156
  error.code = code
151
157
  } catch {
@@ -182,7 +188,10 @@ function exposeWriteError(error) {
182
188
  export function extractData(a, b) {
183
189
  const root = isNode(a) ? a : document
184
190
  const source = isNode(a) ? b : a
185
- if (source === undefined) return engineMod.extract(createDomAdapter(root), root, defaultRules(root))
191
+ if (source === undefined) {
192
+ const { rules, version } = defaultRules(root)
193
+ return engineMod.extract(createDomAdapter(root), root, rules, withCodecs({ version }))
194
+ }
186
195
  return engine.bind(root, source).get()
187
196
  }
188
197
 
@@ -193,8 +202,8 @@ export function applyData(root, data, source) {
193
202
  if (!isNode(root)) {
194
203
  throw new Error('hyper-html-api: applyData(root, data, source?) needs a DOM root as the first argument.')
195
204
  }
196
- const rules = source === undefined ? defaultRules(root) : engine.bind(root, source).rules
197
- guardedApply(root, rules, data)
205
+ const { rules, version } = source === undefined ? defaultRules(root) : engine.bind(root, source)
206
+ guardedApply(root, rules, data, version)
198
207
  } catch (error) {
199
208
  throw exposeWriteError(error)
200
209
  }
@@ -10,13 +10,15 @@ import {
10
10
  ShapeMismatch,
11
11
  } from './errors.js'
12
12
  import { listDiff } from './diff.js'
13
- import { ruleAttrIndex, splitRule } from './rule-syntax.js'
13
+ import { codecOf, ruleAttrIndex, splitRule } from './rule-syntax.js'
14
14
  import { isCustomPropertyName, setStyleVar } from './style-vars.js'
15
+ import { checkCodecRules, codecForRule } from './codecs.js'
15
16
 
16
17
  export function apply(adapter, root, rules, data, opts = {}) {
17
18
  if (Object.prototype.hasOwnProperty.call(opts, 'exclude') && adapter.semanticExclude === false) {
18
19
  throw new Error('This adapter does not support semantic exclude queries; omit exclude or use the DOM adapter.')
19
20
  }
21
+ checkCodecRules(rules, opts)
20
22
  const mismatches = []
21
23
  validateShape(rules, data, [], mismatches)
22
24
  if (mismatches.length) throw new ShapeMismatch(mismatches)
@@ -64,6 +66,13 @@ function applyScalar(adapter, ctx, rule, value, trace, opts) {
64
66
  return ctx
65
67
  }
66
68
 
69
+ const codec = codecForRule(rule, opts)
70
+ if (codec) {
71
+ const matches = codec.selector ? adapter.find(ctx, codec.selector, opts) : [ctx]
72
+ if (matches.length) codec.codec.write(adapter, matches[0], value, { query: opts.query })
73
+ return ctx
74
+ }
75
+
67
76
  if (rule.startsWith('@')) {
68
77
  return writePropOrAttr(adapter, ctx, rule.slice(1), value)
69
78
  }
@@ -156,6 +165,8 @@ function validateShape(rule, value, path, mismatches) {
156
165
  if (value === undefined) return
157
166
 
158
167
  if (typeof rule === 'string') {
168
+ // A codec judges its own value when it writes.
169
+ if (codecOf(rule)) return
159
170
  if (rule.endsWith('[]')) {
160
171
  if (!Array.isArray(value)) {
161
172
  mismatches.push({ path: pathStr(path), expected: 'array', got: typeofX(value) })
@@ -0,0 +1,80 @@
1
+ import { MAX_RULE_DEPTH, RulesParseError, UnknownCodec } from './errors.js'
2
+ import { codecOf } from './rule-syntax.js'
3
+
4
+ // A codec reads and writes one element's whole value as JSON, for content the
5
+ // rule grammar cannot describe (a sheet's grid). It is named by the rule form
6
+ // "selector@codec(name)", valid only under data-rules-version="2" and only as the
7
+ // value of a top level key.
8
+ //
9
+ // codec = {
10
+ // name,
11
+ // read(adapter, node, ctx) -> JSON value ctx = { query?, view?, region? }
12
+ // write(adapter, node, value, ctx) -> void | { regions }
13
+ // patch?(adapter, node, ops, ctx) -> { changed, stale, regions }
14
+ // same(written, reread) -> boolean
15
+ // styles?: string[]
16
+ // cut?(src, marker) -> { rest, regions }
17
+ // }
18
+
19
+ const NAME_RE = /^[a-z][a-z0-9-]*$/
20
+ const registry = new Map()
21
+
22
+ // The browser registry. Hosts pass `codecs` per call instead, so no state is
23
+ // shared between requests.
24
+ export function registerCodec(name, codec) {
25
+ if (typeof name !== 'string' || !NAME_RE.test(name)) {
26
+ throw new Error(`hyper-html-api: invalid codec name ${JSON.stringify(name)} (must match ${NAME_RE})`)
27
+ }
28
+ if (!codec || typeof codec.read !== 'function' || typeof codec.write !== 'function' || typeof codec.same !== 'function') {
29
+ throw new Error(`hyper-html-api: codec "${name}" needs read, write and same functions`)
30
+ }
31
+ registry.set(name, codec)
32
+ }
33
+
34
+ export function getCodec(name) {
35
+ return registry.get(name) ?? null
36
+ }
37
+
38
+ export function registeredCodecs() {
39
+ return Object.fromEntries(registry)
40
+ }
41
+
42
+ export const NEEDS_VERSION_2 = 'The @codec(name) rule form needs a rules tag with data-rules-version="2".'
43
+ export const NEEDS_TOP_LEVEL = 'A @codec(name) rule must be the value of a top level key.'
44
+
45
+ // The codec a rule names, or null for any other rule. Assumes checkCodecRules
46
+ // already accepted the tree.
47
+ export function codecForRule(rule, opts) {
48
+ const form = codecOf(rule)
49
+ return form ? { ...form, codec: opts.codecs[form.name] } : null
50
+ }
51
+
52
+ // Walks the whole rule tree once, before anything is read or written, so a
53
+ // rules tree the call cannot honor refuses without a partial write. A codec
54
+ // rule read as a plain attribute would return null for the whole value, and a
55
+ // null that reaches a draft store or a guard baseline looks like the owner
56
+ // emptied it, which is why every refusal here is loud.
57
+ export function checkCodecRules(rules, opts = {}) {
58
+ const version = opts.version ?? '1'
59
+ const codecs = opts.codecs ?? {}
60
+ walk(rules, 0)
61
+
62
+ function walk(rule, depth) {
63
+ if (depth > MAX_RULE_DEPTH) return
64
+ if (typeof rule === 'string') {
65
+ const form = codecOf(rule)
66
+ if (!form) return
67
+ if (version !== '2') throw new RulesParseError(NEEDS_VERSION_2)
68
+ if (depth !== 1) throw new RulesParseError(NEEDS_TOP_LEVEL)
69
+ if (!Object.prototype.hasOwnProperty.call(codecs, form.name)) throw new UnknownCodec(form.name)
70
+ return
71
+ }
72
+ if (Array.isArray(rule)) {
73
+ walk(rule[1], depth + 1)
74
+ return
75
+ }
76
+ if (rule && typeof rule === 'object') {
77
+ for (const sub of Object.values(rule)) walk(sub, depth + 1)
78
+ }
79
+ }
80
+ }
@@ -8,7 +8,7 @@ export class RulesParseError extends Error {
8
8
 
9
9
  export class UnknownRulesVersion extends Error {
10
10
  constructor(version) {
11
- super(`unknown rules version: ${version}. Library supports "1".`)
11
+ super(`unknown rules version: ${version}. Library supports "1" and "2".`)
12
12
  this.name = 'UnknownRulesVersion'
13
13
  this.version = version
14
14
  }
@@ -92,3 +92,22 @@ export class WriteRejected extends Error {
92
92
  this.unmatched = unmatched
93
93
  }
94
94
  }
95
+
96
+ export class UnknownCodec extends Error {
97
+ constructor(name) {
98
+ super(`unknown codec "${name}": no codec by that name is given to this call`)
99
+ this.name = 'UnknownCodec'
100
+ this.code = 'unknown-codec'
101
+ this.codec = name
102
+ }
103
+ }
104
+
105
+ // Thrown by a codec that will not take a write, with its own code (for
106
+ // example "sheet-not-canonical"). A client error, like UnknownCodec.
107
+ export class CodecRefused extends Error {
108
+ constructor(code, message) {
109
+ super(message)
110
+ this.name = 'CodecRefused'
111
+ this.code = code
112
+ }
113
+ }
@@ -2,11 +2,13 @@ import { DOM_PROPERTIES_SET } from './dom-properties.js'
2
2
  import { MaxRuleDepthExceeded, MAX_RULE_DEPTH } from './errors.js'
3
3
  import { ruleAttrIndex, splitRule } from './rule-syntax.js'
4
4
  import { isCustomPropertyName, readStyleVar } from './style-vars.js'
5
+ import { checkCodecRules, codecForRule } from './codecs.js'
5
6
 
6
7
  export function extract(adapter, root, rules, opts = {}) {
7
8
  if (Object.prototype.hasOwnProperty.call(opts, 'exclude') && adapter.semanticExclude === false) {
8
9
  throw new Error('This adapter does not support semantic exclude queries; omit exclude or use the DOM adapter.')
9
10
  }
11
+ checkCodecRules(rules, opts)
10
12
  return extractAt(adapter, root, rules, { depth: 0, path: [] }, opts)
11
13
  }
12
14
 
@@ -57,6 +59,9 @@ function extractScalar(adapter, ctx, rule, trace, opts) {
57
59
  return matches.map((n) => adapter.text(n))
58
60
  }
59
61
 
62
+ const codec = codecForRule(rule, opts)
63
+ if (codec) return readCodec(adapter, ctx, codec, opts)
64
+
60
65
  if (rule.startsWith('@')) {
61
66
  return readPropOrAttr(adapter, ctx, rule.slice(1))
62
67
  }
@@ -79,6 +84,12 @@ function extractScalar(adapter, ctx, rule, trace, opts) {
79
84
  return matches.length === 0 ? null : adapter.text(matches[0])
80
85
  }
81
86
 
87
+ function readCodec(adapter, ctx, { selector, codec }, opts) {
88
+ const matches = selector ? adapter.find(ctx, selector, opts) : [ctx]
89
+ if (matches.length === 0) return null
90
+ return codec.read(adapter, matches[0], { query: opts.query, view: opts.view, region: opts.region })
91
+ }
92
+
82
93
  // The path is copied because the caller is handed it and every nested path is
83
94
  // built from the same array. A throwing hook must not abandon the extract.
84
95
  function reportRows(opts, trace, nodes) {
@@ -6,7 +6,9 @@ export { extract } from './extract.js'
6
6
  export { apply } from './apply.js'
7
7
  export { findRulesIn } from './rules-tag.js'
8
8
  export { parseStrict, parseRelaxed } from './rules.js'
9
- export { ruleAttrIndex, splitRule } from './rule-syntax.js'
9
+ export { ruleAttrIndex, splitRule, codecOf } from './rule-syntax.js'
10
+ export { registerCodec, getCodec, registeredCodecs, checkCodecRules } from './codecs.js'
11
+ export { SUPPORTED_VERSION, SUPPORTED_VERSIONS } from './rules-tag.js'
10
12
  export * as errors from './errors.js'
11
13
  export { DOM_PROPERTIES, DOM_PROPERTIES_SET, DOM_PROPERTIES_READ_ONLY_SET } from './dom-properties.js'
12
14
  export { WRITE_POLICY, guardAdapter, checkAttribute, checkCustomPropertyValue, urlScheme } from './write-policy.js'
@@ -21,7 +23,7 @@ export { trackAdapter, spliceDocument } from './splice.js'
21
23
  // For a Document root or the cheerio $.root(), ownerDocument is absent → no-op.
22
24
  export function resolveRules(adapter, root, source) {
23
25
  if (source && typeof source === 'object') {
24
- return { rules: source, tagNode: null }
26
+ return { rules: source, tagNode: null, version: '1' }
25
27
  }
26
28
  if (typeof source === 'string') {
27
29
  const searchRoot = (root && root.ownerDocument) ? root.ownerDocument : root
@@ -36,11 +38,13 @@ export function bind(adapter, root, source, opts) {
36
38
  const what = typeof source === 'string' ? `data-rules-name~="${source}"` : 'the provided rules object'
37
39
  throw new Error(`hyper-html-api: could not resolve rules for ${what}`)
38
40
  }
39
- const { rules, tagNode } = found
41
+ const { rules, tagNode, version } = found
42
+ const callOpts = { version, ...opts }
40
43
  return {
41
44
  rules,
42
45
  tagNode, // informational only; null for a literal object
43
- get: () => extract(adapter, root, rules, opts), // opts (skip/templateAttr) forwarded
44
- set: (data) => apply(adapter, root, rules, data, opts),
46
+ version,
47
+ get: () => extract(adapter, root, rules, callOpts), // opts (skip/templateAttr/codecs) forwarded
48
+ set: (data) => apply(adapter, root, rules, data, callOpts),
45
49
  }
46
50
  }
@@ -4,7 +4,7 @@ import {
4
4
  coercePropValue,
5
5
  } from './dom-properties.js'
6
6
  import { WriteNotRoundTripped } from './errors.js'
7
- import { ruleAttrIndex, splitRule } from './rule-syntax.js'
7
+ import { codecOf, ruleAttrIndex, splitRule } from './rule-syntax.js'
8
8
  import { isCustomPropertyName } from './style-vars.js'
9
9
 
10
10
  // Props whose read-side value keeps its whitespace. Every other text rule reads
@@ -18,22 +18,28 @@ const UNTRIMMED_PROPS = new Set(['textContent', 'innerText'])
18
18
  // WriteNotRoundTripped instead of returning a false success. Only the supplied
19
19
  // projection is checked: omitted keys and undefined are skipped exactly as the
20
20
  // writer skips them, which is what keeps partial updates working.
21
- export function assertRoundTrip(rules, requested, actual) {
22
- check(rules, requested, actual, [])
21
+ // A codec value is compared by its codec's own same(), from `codecs`.
22
+ export function assertRoundTrip(rules, requested, actual, codecs = {}) {
23
+ check(rules, requested, actual, [], codecs)
23
24
  }
24
25
 
25
- function check(rule, want, got, path) {
26
+ function check(rule, want, got, path, codecs) {
26
27
  if (want === undefined) return
27
28
  if (typeof rule === 'string') {
29
+ const form = codecOf(rule)
30
+ if (form) {
31
+ if (!codecs[form.name].same(want, got)) fail(path)
32
+ return
33
+ }
28
34
  checkScalarRule(rule, want, got, path)
29
35
  return
30
36
  }
31
37
  if (Array.isArray(rule)) {
32
- checkList(rule[1], want, got, path)
38
+ checkList(rule[1], want, got, path, codecs)
33
39
  return
34
40
  }
35
41
  if (typeof rule === 'object' && rule !== null) {
36
- checkObject(rule, want, got, path)
42
+ checkObject(rule, want, got, path, codecs)
37
43
  }
38
44
  }
39
45
 
@@ -71,16 +77,16 @@ function checkTextList(want, got, path) {
71
77
  for (let i = 0; i < want.length; i++) checkText(want[i], got[i], [...path, i])
72
78
  }
73
79
 
74
- function checkObject(rule, want, got, path) {
80
+ function checkObject(rule, want, got, path, codecs) {
75
81
  if (!isPlainObject(want) || !isPlainObject(got)) fail(path)
76
82
  for (const [key, sub] of Object.entries(rule)) {
77
- check(sub, want[key], got[key], [...path, key])
83
+ check(sub, want[key], got[key], [...path, key], codecs)
78
84
  }
79
85
  }
80
86
 
81
- function checkList(shape, want, got, path) {
87
+ function checkList(shape, want, got, path, codecs) {
82
88
  if (!Array.isArray(want) || !Array.isArray(got) || want.length !== got.length) fail(path)
83
- for (let i = 0; i < want.length; i++) check(shape, want[i], got[i], [...path, i])
89
+ for (let i = 0; i < want.length; i++) check(shape, want[i], got[i], [...path, i], codecs)
84
90
  }
85
91
 
86
92
  function checkPropOrAttr(name, want, got, path) {
@@ -10,6 +10,8 @@
10
10
  // rule into `a[href="mailto:hi` and `example.com"]`, which reads downstream as
11
11
  // an invalid selector when the rule the author wrote was correct.
12
12
 
13
+ import { RulesParseError } from './errors.js'
14
+
13
15
  // Index of the separator "@", or -1 when the rule has no prop part.
14
16
  export function ruleAttrIndex(rule) {
15
17
  let depth = 0
@@ -122,3 +124,17 @@ function isSingleCompound(selector, start) {
122
124
  }
123
125
  return true
124
126
  }
127
+
128
+ const CODEC_PROP = /^codec\(([a-z][a-z0-9-]*)\)$/
129
+
130
+ // { selector, name } when a scalar rule calls a codec, else null.
131
+ export function codecOf(rule) {
132
+ if (typeof rule !== 'string') return null
133
+ const at = ruleAttrIndex(rule)
134
+ if (at === -1) return null
135
+ const match = CODEC_PROP.exec(rule.slice(at + 1))
136
+ if (!match) return null
137
+ const { selector, all } = splitRule(rule)
138
+ if (all) throw new RulesParseError('A @codec(name) rule cannot use [*].')
139
+ return { selector, name: match[1] }
140
+ }
@@ -2,6 +2,7 @@ import { parseRelaxed } from './rules.js'
2
2
  import { UnknownRulesVersion } from './errors.js'
3
3
 
4
4
  export const SUPPORTED_VERSION = '1'
5
+ export const SUPPORTED_VERSIONS = ['1', '2']
5
6
  const TOKEN_RE = /^[a-zA-Z0-9_-]+$/
6
7
 
7
8
  // findRulesIn(adapter, root) → first script[data-rules-name] (tooling/no-token)
@@ -29,12 +30,12 @@ export function findRulesIn(adapter, root, token) {
29
30
 
30
31
  const tagNode = candidates[0]
31
32
  const version = adapter.attr(tagNode, 'data-rules-version')
32
- if (version !== SUPPORTED_VERSION) throw new UnknownRulesVersion(version)
33
+ if (!SUPPORTED_VERSIONS.includes(version)) throw new UnknownRulesVersion(version)
33
34
 
34
35
  // Script tag bodies accept the same relaxed JSON syntax as the ?data=
35
36
  // URL parameter: unquoted keys, single-quoted strings, trailing commas.
36
37
  // parseRelaxed tries strict JSON.parse first, so valid JSON keeps
37
38
  // parsing identically.
38
39
  const rules = parseRelaxed(adapter.text(tagNode))
39
- return { rules, tagNode }
40
+ return { rules, tagNode, version }
40
41
  }
@@ -1,4 +1,5 @@
1
1
  import { splitRule } from './rule-syntax.js'
2
+ import { checkCodecRules } from './codecs.js'
2
3
 
3
4
  function isPlainObject(v) {
4
5
  return v !== null && typeof v === 'object' && !Array.isArray(v)
@@ -13,6 +14,7 @@ function label(path) {
13
14
  // Shape problems are left to apply(), which already reports them. Inside list
14
15
  // items only unknown keys are checked, because new rows do not exist yet.
15
16
  export function planWrite(adapter, root, rules, data, opts = {}) {
17
+ checkCodecRules(rules, opts)
16
18
  const unknownKeys = []
17
19
  const unmatched = []
18
20
 
@@ -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
+ }