@projektemacher/storymapjs 0.10.7 → 0.11.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 (587) hide show
  1. package/CHANGELOG +431 -19
  2. package/README.md +168 -17
  3. package/dist/assets/demo-bWnOyfUw.js +1 -0
  4. package/dist/assets/harness-C3JUv5Ii.js +1 -0
  5. package/dist/assets/harness-multi-XZNex3tb.js +1 -0
  6. package/dist/assets/harness-support-CjfZIVeb.js +1 -0
  7. package/dist/assets/index-wHPqGrBS.js +1 -0
  8. package/dist/assets/main-DQ32hWBM.js +17 -0
  9. package/dist/context.json +23 -9
  10. package/dist/css/fonts/files/amatic-sc-cyrillic-400-normal.woff2 +0 -0
  11. package/dist/css/fonts/files/amatic-sc-cyrillic-700-normal.woff2 +0 -0
  12. package/dist/css/fonts/files/amatic-sc-hebrew-400-normal.woff2 +0 -0
  13. package/dist/css/fonts/files/amatic-sc-hebrew-700-normal.woff2 +0 -0
  14. package/dist/css/fonts/files/amatic-sc-vietnamese-400-normal.woff2 +0 -0
  15. package/dist/css/fonts/files/amatic-sc-vietnamese-700-normal.woff2 +0 -0
  16. package/dist/css/fonts/files/bitter-cyrillic-100-italic.woff2 +0 -0
  17. package/dist/css/fonts/files/bitter-cyrillic-100-normal.woff2 +0 -0
  18. package/dist/css/fonts/files/bitter-cyrillic-200-italic.woff2 +0 -0
  19. package/dist/css/fonts/files/bitter-cyrillic-200-normal.woff2 +0 -0
  20. package/dist/css/fonts/files/bitter-cyrillic-300-italic.woff2 +0 -0
  21. package/dist/css/fonts/files/bitter-cyrillic-300-normal.woff2 +0 -0
  22. package/dist/css/fonts/files/bitter-cyrillic-400-italic.woff2 +0 -0
  23. package/dist/css/fonts/files/bitter-cyrillic-400-normal.woff2 +0 -0
  24. package/dist/css/fonts/files/bitter-cyrillic-500-italic.woff2 +0 -0
  25. package/dist/css/fonts/files/bitter-cyrillic-500-normal.woff2 +0 -0
  26. package/dist/css/fonts/files/bitter-cyrillic-600-italic.woff2 +0 -0
  27. package/dist/css/fonts/files/bitter-cyrillic-600-normal.woff2 +0 -0
  28. package/dist/css/fonts/files/bitter-cyrillic-700-italic.woff2 +0 -0
  29. package/dist/css/fonts/files/bitter-cyrillic-700-normal.woff2 +0 -0
  30. package/dist/css/fonts/files/bitter-cyrillic-800-italic.woff2 +0 -0
  31. package/dist/css/fonts/files/bitter-cyrillic-800-normal.woff2 +0 -0
  32. package/dist/css/fonts/files/bitter-cyrillic-900-italic.woff2 +0 -0
  33. package/dist/css/fonts/files/bitter-cyrillic-900-normal.woff2 +0 -0
  34. package/dist/css/fonts/files/bitter-cyrillic-ext-100-italic.woff2 +0 -0
  35. package/dist/css/fonts/files/bitter-cyrillic-ext-100-normal.woff2 +0 -0
  36. package/dist/css/fonts/files/bitter-cyrillic-ext-200-italic.woff2 +0 -0
  37. package/dist/css/fonts/files/bitter-cyrillic-ext-200-normal.woff2 +0 -0
  38. package/dist/css/fonts/files/bitter-cyrillic-ext-300-italic.woff2 +0 -0
  39. package/dist/css/fonts/files/bitter-cyrillic-ext-300-normal.woff2 +0 -0
  40. package/dist/css/fonts/files/bitter-cyrillic-ext-400-italic.woff2 +0 -0
  41. package/dist/css/fonts/files/bitter-cyrillic-ext-400-normal.woff2 +0 -0
  42. package/dist/css/fonts/files/bitter-cyrillic-ext-500-italic.woff2 +0 -0
  43. package/dist/css/fonts/files/bitter-cyrillic-ext-500-normal.woff2 +0 -0
  44. package/dist/css/fonts/files/bitter-cyrillic-ext-600-italic.woff2 +0 -0
  45. package/dist/css/fonts/files/bitter-cyrillic-ext-600-normal.woff2 +0 -0
  46. package/dist/css/fonts/files/bitter-cyrillic-ext-700-italic.woff2 +0 -0
  47. package/dist/css/fonts/files/bitter-cyrillic-ext-700-normal.woff2 +0 -0
  48. package/dist/css/fonts/files/bitter-cyrillic-ext-800-italic.woff2 +0 -0
  49. package/dist/css/fonts/files/bitter-cyrillic-ext-800-normal.woff2 +0 -0
  50. package/dist/css/fonts/files/bitter-cyrillic-ext-900-italic.woff2 +0 -0
  51. package/dist/css/fonts/files/bitter-cyrillic-ext-900-normal.woff2 +0 -0
  52. package/dist/css/fonts/files/bitter-cyrillic-ext-wght-italic.woff2 +0 -0
  53. package/dist/css/fonts/files/bitter-cyrillic-ext-wght-normal.woff2 +0 -0
  54. package/dist/css/fonts/files/bitter-cyrillic-wght-italic.woff2 +0 -0
  55. package/dist/css/fonts/files/bitter-cyrillic-wght-normal.woff2 +0 -0
  56. package/dist/css/fonts/files/bitter-vietnamese-100-italic.woff2 +0 -0
  57. package/dist/css/fonts/files/bitter-vietnamese-100-normal.woff2 +0 -0
  58. package/dist/css/fonts/files/bitter-vietnamese-200-italic.woff2 +0 -0
  59. package/dist/css/fonts/files/bitter-vietnamese-200-normal.woff2 +0 -0
  60. package/dist/css/fonts/files/bitter-vietnamese-300-italic.woff2 +0 -0
  61. package/dist/css/fonts/files/bitter-vietnamese-300-normal.woff2 +0 -0
  62. package/dist/css/fonts/files/bitter-vietnamese-400-italic.woff2 +0 -0
  63. package/dist/css/fonts/files/bitter-vietnamese-400-normal.woff2 +0 -0
  64. package/dist/css/fonts/files/bitter-vietnamese-500-italic.woff2 +0 -0
  65. package/dist/css/fonts/files/bitter-vietnamese-500-normal.woff2 +0 -0
  66. package/dist/css/fonts/files/bitter-vietnamese-600-italic.woff2 +0 -0
  67. package/dist/css/fonts/files/bitter-vietnamese-600-normal.woff2 +0 -0
  68. package/dist/css/fonts/files/bitter-vietnamese-700-italic.woff2 +0 -0
  69. package/dist/css/fonts/files/bitter-vietnamese-700-normal.woff2 +0 -0
  70. package/dist/css/fonts/files/bitter-vietnamese-800-italic.woff2 +0 -0
  71. package/dist/css/fonts/files/bitter-vietnamese-800-normal.woff2 +0 -0
  72. package/dist/css/fonts/files/bitter-vietnamese-900-italic.woff2 +0 -0
  73. package/dist/css/fonts/files/bitter-vietnamese-900-normal.woff2 +0 -0
  74. package/dist/css/fonts/files/bitter-vietnamese-wght-italic.woff2 +0 -0
  75. package/dist/css/fonts/files/bitter-vietnamese-wght-normal.woff2 +0 -0
  76. package/dist/css/fonts/files/cairo-arabic-200-normal.woff2 +0 -0
  77. package/dist/css/fonts/files/cairo-arabic-300-normal.woff2 +0 -0
  78. package/dist/css/fonts/files/cairo-arabic-400-normal.woff2 +0 -0
  79. package/dist/css/fonts/files/cairo-arabic-500-normal.woff2 +0 -0
  80. package/dist/css/fonts/files/cairo-arabic-600-normal.woff2 +0 -0
  81. package/dist/css/fonts/files/cairo-arabic-700-normal.woff2 +0 -0
  82. package/dist/css/fonts/files/cairo-arabic-800-normal.woff2 +0 -0
  83. package/dist/css/fonts/files/cairo-arabic-900-normal.woff2 +0 -0
  84. package/dist/css/fonts/files/cairo-latin-200-normal.woff2 +0 -0
  85. package/dist/css/fonts/files/cairo-latin-300-normal.woff2 +0 -0
  86. package/dist/css/fonts/files/cairo-latin-400-normal.woff2 +0 -0
  87. package/dist/css/fonts/files/cairo-latin-500-normal.woff2 +0 -0
  88. package/dist/css/fonts/files/cairo-latin-600-normal.woff2 +0 -0
  89. package/dist/css/fonts/files/cairo-latin-700-normal.woff2 +0 -0
  90. package/dist/css/fonts/files/cairo-latin-800-normal.woff2 +0 -0
  91. package/dist/css/fonts/files/cairo-latin-900-normal.woff2 +0 -0
  92. package/dist/css/fonts/files/cairo-latin-ext-200-normal.woff2 +0 -0
  93. package/dist/css/fonts/files/cairo-latin-ext-300-normal.woff2 +0 -0
  94. package/dist/css/fonts/files/cairo-latin-ext-400-normal.woff2 +0 -0
  95. package/dist/css/fonts/files/cairo-latin-ext-500-normal.woff2 +0 -0
  96. package/dist/css/fonts/files/cairo-latin-ext-600-normal.woff2 +0 -0
  97. package/dist/css/fonts/files/cairo-latin-ext-700-normal.woff2 +0 -0
  98. package/dist/css/fonts/files/cairo-latin-ext-800-normal.woff2 +0 -0
  99. package/dist/css/fonts/files/cairo-latin-ext-900-normal.woff2 +0 -0
  100. package/dist/css/fonts/files/dancing-script-vietnamese-wght-normal.woff2 +0 -0
  101. package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-italic.woff2 +0 -0
  102. package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-normal.woff2 +0 -0
  103. package/dist/css/fonts/files/eb-garamond-cyrillic-wght-italic.woff2 +0 -0
  104. package/dist/css/fonts/files/eb-garamond-cyrillic-wght-normal.woff2 +0 -0
  105. package/dist/css/fonts/files/eb-garamond-greek-ext-wght-italic.woff2 +0 -0
  106. package/dist/css/fonts/files/eb-garamond-greek-ext-wght-normal.woff2 +0 -0
  107. package/dist/css/fonts/files/eb-garamond-greek-wght-italic.woff2 +0 -0
  108. package/dist/css/fonts/files/eb-garamond-greek-wght-normal.woff2 +0 -0
  109. package/dist/css/fonts/files/eb-garamond-vietnamese-wght-italic.woff2 +0 -0
  110. package/dist/css/fonts/files/eb-garamond-vietnamese-wght-normal.woff2 +0 -0
  111. package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-italic.woff2 +0 -0
  112. package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-normal.woff2 +0 -0
  113. package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-italic.woff2 +0 -0
  114. package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-normal.woff2 +0 -0
  115. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-italic.woff2 +0 -0
  116. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-normal.woff2 +0 -0
  117. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-italic.woff2 +0 -0
  118. package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-normal.woff2 +0 -0
  119. package/dist/css/fonts/files/gentium-book-plus-greek-400-italic.woff2 +0 -0
  120. package/dist/css/fonts/files/gentium-book-plus-greek-400-normal.woff2 +0 -0
  121. package/dist/css/fonts/files/gentium-book-plus-greek-700-italic.woff2 +0 -0
  122. package/dist/css/fonts/files/gentium-book-plus-greek-700-normal.woff2 +0 -0
  123. package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-italic.woff2 +0 -0
  124. package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-normal.woff2 +0 -0
  125. package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-italic.woff2 +0 -0
  126. package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-normal.woff2 +0 -0
  127. package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-italic.woff2 +0 -0
  128. package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-normal.woff2 +0 -0
  129. package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-italic.woff2 +0 -0
  130. package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-normal.woff2 +0 -0
  131. package/dist/css/fonts/files/old-standard-tt-cyrillic-400-italic.woff2 +0 -0
  132. package/dist/css/fonts/files/old-standard-tt-cyrillic-400-normal.woff2 +0 -0
  133. package/dist/css/fonts/files/old-standard-tt-cyrillic-700-normal.woff2 +0 -0
  134. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-italic.woff2 +0 -0
  135. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-normal.woff2 +0 -0
  136. package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-700-normal.woff2 +0 -0
  137. package/dist/css/fonts/files/old-standard-tt-vietnamese-400-italic.woff2 +0 -0
  138. package/dist/css/fonts/files/old-standard-tt-vietnamese-400-normal.woff2 +0 -0
  139. package/dist/css/fonts/files/old-standard-tt-vietnamese-700-normal.woff2 +0 -0
  140. package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-italic.woff2 +0 -0
  141. package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-normal.woff2 +0 -0
  142. package/dist/css/fonts/files/open-sans-cyrillic-wght-italic.woff2 +0 -0
  143. package/dist/css/fonts/files/open-sans-cyrillic-wght-normal.woff2 +0 -0
  144. package/dist/css/fonts/files/open-sans-greek-ext-wght-italic.woff2 +0 -0
  145. package/dist/css/fonts/files/open-sans-greek-ext-wght-normal.woff2 +0 -0
  146. package/dist/css/fonts/files/open-sans-greek-wght-italic.woff2 +0 -0
  147. package/dist/css/fonts/files/open-sans-greek-wght-normal.woff2 +0 -0
  148. package/dist/css/fonts/files/open-sans-hebrew-wght-italic.woff2 +0 -0
  149. package/dist/css/fonts/files/open-sans-hebrew-wght-normal.woff2 +0 -0
  150. package/dist/css/fonts/files/open-sans-vietnamese-wght-italic.woff2 +0 -0
  151. package/dist/css/fonts/files/open-sans-vietnamese-wght-normal.woff2 +0 -0
  152. package/dist/css/fonts/files/playfair-display-cyrillic-wght-italic.woff2 +0 -0
  153. package/dist/css/fonts/files/playfair-display-cyrillic-wght-normal.woff2 +0 -0
  154. package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-italic.woff2 +0 -0
  155. package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-normal.woff2 +0 -0
  156. package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-italic.woff2 +0 -0
  157. package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-normal.woff2 +0 -0
  158. package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-italic.woff2 +0 -0
  159. package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-normal.woff2 +0 -0
  160. package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-italic.woff2 +0 -0
  161. package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-normal.woff2 +0 -0
  162. package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-italic.woff2 +0 -0
  163. package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-normal.woff2 +0 -0
  164. package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-italic.woff2 +0 -0
  165. package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-normal.woff2 +0 -0
  166. package/dist/css/fonts/files/playfair-display-vietnamese-wght-italic.woff2 +0 -0
  167. package/dist/css/fonts/files/playfair-display-vietnamese-wght-normal.woff2 +0 -0
  168. package/dist/css/fonts/files/pt-sans-cyrillic-400-italic.woff2 +0 -0
  169. package/dist/css/fonts/files/pt-sans-cyrillic-400-normal.woff2 +0 -0
  170. package/dist/css/fonts/files/pt-sans-cyrillic-700-italic.woff2 +0 -0
  171. package/dist/css/fonts/files/pt-sans-cyrillic-700-normal.woff2 +0 -0
  172. package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-italic.woff2 +0 -0
  173. package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-normal.woff2 +0 -0
  174. package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-italic.woff2 +0 -0
  175. package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-normal.woff2 +0 -0
  176. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-400-normal.woff2 +0 -0
  177. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-700-normal.woff2 +0 -0
  178. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-400-normal.woff2 +0 -0
  179. package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-700-normal.woff2 +0 -0
  180. package/dist/css/fonts/files/pt-serif-cyrillic-400-italic.woff2 +0 -0
  181. package/dist/css/fonts/files/pt-serif-cyrillic-400-normal.woff2 +0 -0
  182. package/dist/css/fonts/files/pt-serif-cyrillic-700-italic.woff2 +0 -0
  183. package/dist/css/fonts/files/pt-serif-cyrillic-700-normal.woff2 +0 -0
  184. package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-italic.woff2 +0 -0
  185. package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-normal.woff2 +0 -0
  186. package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-italic.woff2 +0 -0
  187. package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-normal.woff2 +0 -0
  188. package/dist/css/fonts/files/raleway-cyrillic-ext-wght-italic.woff2 +0 -0
  189. package/dist/css/fonts/files/raleway-cyrillic-ext-wght-normal.woff2 +0 -0
  190. package/dist/css/fonts/files/raleway-cyrillic-wght-italic.woff2 +0 -0
  191. package/dist/css/fonts/files/raleway-cyrillic-wght-normal.woff2 +0 -0
  192. package/dist/css/fonts/files/raleway-vietnamese-wght-italic.woff2 +0 -0
  193. package/dist/css/fonts/files/raleway-vietnamese-wght-normal.woff2 +0 -0
  194. package/dist/css/fonts/files/roboto-slab-cyrillic-ext-wght-normal.woff2 +0 -0
  195. package/dist/css/fonts/files/roboto-slab-cyrillic-wght-normal.woff2 +0 -0
  196. package/dist/css/fonts/files/roboto-slab-greek-ext-wght-normal.woff2 +0 -0
  197. package/dist/css/fonts/files/roboto-slab-greek-wght-normal.woff2 +0 -0
  198. package/dist/css/fonts/files/roboto-slab-vietnamese-wght-normal.woff2 +0 -0
  199. package/dist/css/fonts/files/ubuntu-cyrillic-300-italic.woff2 +0 -0
  200. package/dist/css/fonts/files/ubuntu-cyrillic-300-normal.woff2 +0 -0
  201. package/dist/css/fonts/files/ubuntu-cyrillic-400-italic.woff2 +0 -0
  202. package/dist/css/fonts/files/ubuntu-cyrillic-400-normal.woff2 +0 -0
  203. package/dist/css/fonts/files/ubuntu-cyrillic-500-italic.woff2 +0 -0
  204. package/dist/css/fonts/files/ubuntu-cyrillic-500-normal.woff2 +0 -0
  205. package/dist/css/fonts/files/ubuntu-cyrillic-700-italic.woff2 +0 -0
  206. package/dist/css/fonts/files/ubuntu-cyrillic-700-normal.woff2 +0 -0
  207. package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-italic.woff2 +0 -0
  208. package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-normal.woff2 +0 -0
  209. package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-italic.woff2 +0 -0
  210. package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-normal.woff2 +0 -0
  211. package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-italic.woff2 +0 -0
  212. package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-normal.woff2 +0 -0
  213. package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-italic.woff2 +0 -0
  214. package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-normal.woff2 +0 -0
  215. package/dist/css/fonts/files/ubuntu-greek-300-italic.woff2 +0 -0
  216. package/dist/css/fonts/files/ubuntu-greek-300-normal.woff2 +0 -0
  217. package/dist/css/fonts/files/ubuntu-greek-400-italic.woff2 +0 -0
  218. package/dist/css/fonts/files/ubuntu-greek-400-normal.woff2 +0 -0
  219. package/dist/css/fonts/files/ubuntu-greek-500-italic.woff2 +0 -0
  220. package/dist/css/fonts/files/ubuntu-greek-500-normal.woff2 +0 -0
  221. package/dist/css/fonts/files/ubuntu-greek-700-italic.woff2 +0 -0
  222. package/dist/css/fonts/files/ubuntu-greek-700-normal.woff2 +0 -0
  223. package/dist/css/fonts/files/ubuntu-greek-ext-300-italic.woff2 +0 -0
  224. package/dist/css/fonts/files/ubuntu-greek-ext-300-normal.woff2 +0 -0
  225. package/dist/css/fonts/files/ubuntu-greek-ext-400-italic.woff2 +0 -0
  226. package/dist/css/fonts/files/ubuntu-greek-ext-400-normal.woff2 +0 -0
  227. package/dist/css/fonts/files/ubuntu-greek-ext-500-italic.woff2 +0 -0
  228. package/dist/css/fonts/files/ubuntu-greek-ext-500-normal.woff2 +0 -0
  229. package/dist/css/fonts/files/ubuntu-greek-ext-700-italic.woff2 +0 -0
  230. package/dist/css/fonts/files/ubuntu-greek-ext-700-normal.woff2 +0 -0
  231. package/dist/css/fonts/files/unica-one-vietnamese-400-normal.woff2 +0 -0
  232. package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-italic.woff2 +0 -0
  233. package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-normal.woff2 +0 -0
  234. package/dist/css/fonts/files/vollkorn-cyrillic-wght-italic.woff2 +0 -0
  235. package/dist/css/fonts/files/vollkorn-cyrillic-wght-normal.woff2 +0 -0
  236. package/dist/css/fonts/files/vollkorn-greek-wght-italic.woff2 +0 -0
  237. package/dist/css/fonts/files/vollkorn-greek-wght-normal.woff2 +0 -0
  238. package/dist/css/fonts/files/vollkorn-vietnamese-wght-italic.woff2 +0 -0
  239. package/dist/css/fonts/files/vollkorn-vietnamese-wght-normal.woff2 +0 -0
  240. package/dist/css/fonts/font.abril-droidsans.css +3 -0
  241. package/dist/css/fonts/font.amatic-andika.css +57 -0
  242. package/dist/css/fonts/font.bitter-raleway.css +111 -0
  243. package/dist/css/fonts/font.cairo.css +312 -0
  244. package/dist/css/fonts/font.clicker-garamond.css +93 -0
  245. package/dist/css/fonts/font.dancing-ledger.css +12 -0
  246. package/dist/css/fonts/font.default.css +183 -0
  247. package/dist/css/fonts/font.fjalla-average.css +3 -0
  248. package/dist/css/fonts/font.georgia-helvetica.css +3 -0
  249. package/dist/css/fonts/font.knightlab.css +642 -0
  250. package/dist/css/fonts/font.lustria-lato.css +3 -0
  251. package/dist/css/fonts/font.medula-lato.css +3 -0
  252. package/dist/css/fonts/font.oldstandard.css +84 -0
  253. package/dist/css/fonts/font.opensans-gentiumbook.css +183 -0
  254. package/dist/css/fonts/font.playfair-faunaone.css +39 -0
  255. package/dist/css/fonts/font.playfair.css +147 -0
  256. package/dist/css/fonts/font.pt.css +183 -0
  257. package/dist/css/fonts/font.roboto-megrim.css +3 -0
  258. package/dist/css/fonts/font.rufina-sintony.css +3 -0
  259. package/dist/css/fonts/font.ubuntu.css +291 -0
  260. package/dist/css/fonts/font.unicaone-vollkorn.css +84 -0
  261. package/dist/css/storymap.css +1 -1
  262. package/dist/demo.html +3 -2
  263. package/dist/docs/api/.nojekyll +1 -0
  264. package/dist/docs/api/assets/custom.css +47 -0
  265. package/dist/docs/api/assets/hierarchy.js +1 -0
  266. package/dist/docs/api/assets/highlight.css +57 -0
  267. package/dist/docs/api/assets/icons.js +18 -0
  268. package/dist/docs/api/assets/icons.svg +1 -0
  269. package/dist/docs/api/assets/main.js +60 -0
  270. package/dist/docs/api/assets/navigation.js +1 -0
  271. package/dist/docs/api/assets/search.js +1 -0
  272. package/dist/docs/api/assets/style.css +1648 -0
  273. package/dist/docs/api/classes/StoryMap.html +2112 -0
  274. package/dist/docs/api/functions/MediaType.html +51 -0
  275. package/dist/docs/api/functions/isPresentation3Collection.html +47 -0
  276. package/dist/docs/api/functions/isPresentation3Manifest.html +53 -0
  277. package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
  278. package/dist/docs/api/functions/loadCSS.html +63 -0
  279. package/dist/docs/api/functions/manifestToStorymapData.html +49 -0
  280. package/dist/docs/api/functions/setLanguage.html +54 -0
  281. package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
  282. package/dist/docs/api/functions/slideshowVersion.html +48 -0
  283. package/dist/docs/api/functions/storymapToManifest.html +57 -0
  284. package/dist/docs/api/functions/validateStorymap.html +53 -0
  285. package/dist/docs/api/functions/validateStorymapAndReport.html +54 -0
  286. package/dist/docs/api/hierarchy.html +28 -0
  287. package/dist/docs/api/index.html +300 -0
  288. package/dist/docs/api/interfaces/AnimationHandle.html +60 -0
  289. package/dist/docs/api/interfaces/ImagereadyPayload.html +71 -0
  290. package/dist/docs/api/interfaces/LanguageEntry.html +102 -0
  291. package/dist/docs/api/interfaces/LatLngLiteral.html +73 -0
  292. package/dist/docs/api/interfaces/LoadOptions.html +59 -0
  293. package/dist/docs/api/interfaces/MapEvents.html +116 -0
  294. package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
  295. package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
  296. package/dist/docs/api/interfaces/MediaEvents.html +78 -0
  297. package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
  298. package/dist/docs/api/interfaces/MessageEvents.html +72 -0
  299. package/dist/docs/api/interfaces/OlLayer.html +1400 -0
  300. package/dist/docs/api/interfaces/OlMap.html +1656 -0
  301. package/dist/docs/api/interfaces/OlProjection.html +332 -0
  302. package/dist/docs/api/interfaces/OlSource.html +784 -0
  303. package/dist/docs/api/interfaces/OlTileLayer.html +1497 -0
  304. package/dist/docs/api/interfaces/OlVectorLayer.html +1559 -0
  305. package/dist/docs/api/interfaces/OlView.html +1958 -0
  306. package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
  307. package/dist/docs/api/interfaces/SlideEvents.html +78 -0
  308. package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
  309. package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
  310. package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
  311. package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
  312. package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
  313. package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
  314. package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
  315. package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
  316. package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
  317. package/dist/docs/api/interfaces/StorymapData.html +77 -0
  318. package/dist/docs/api/interfaces/StorymapDataWrapper.html +60 -0
  319. package/dist/docs/api/interfaces/StorymapDocument.html +68 -0
  320. package/dist/docs/api/interfaces/StorymapError.html +63 -0
  321. package/dist/docs/api/interfaces/StorymapGeoreference.html +93 -0
  322. package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +74 -0
  323. package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +69 -0
  324. package/dist/docs/api/interfaces/StorymapLanguageMap.html +57 -0
  325. package/dist/docs/api/interfaces/StorymapManifest.html +118 -0
  326. package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +137 -0
  327. package/dist/docs/api/interfaces/StorymapManifestBackground.html +81 -0
  328. package/dist/docs/api/interfaces/StorymapManifestCanvas.html +188 -0
  329. package/dist/docs/api/interfaces/StorymapManifestContentResource.html +97 -0
  330. package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +82 -0
  331. package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +87 -0
  332. package/dist/docs/api/interfaces/StorymapManifestImageService.html +69 -0
  333. package/dist/docs/api/interfaces/StorymapManifestRange.html +75 -0
  334. package/dist/docs/api/interfaces/StorymapManifestService.html +77 -0
  335. package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +69 -0
  336. package/dist/docs/api/interfaces/StorymapManifestStatement.html +63 -0
  337. package/dist/docs/api/interfaces/StorymapMapOptions.html +92 -0
  338. package/dist/docs/api/interfaces/StorymapOptions.html +734 -0
  339. package/dist/docs/api/interfaces/StorymapOverlayLayer.html +157 -0
  340. package/dist/docs/api/interfaces/StorymapSlide.html +186 -0
  341. package/dist/docs/api/interfaces/StorymapSlideBackground.html +72 -0
  342. package/dist/docs/api/interfaces/StorymapSlideLocation.html +161 -0
  343. package/dist/docs/api/interfaces/StorymapSlideMarker.html +103 -0
  344. package/dist/docs/api/interfaces/StorymapSlideMedia.html +149 -0
  345. package/dist/docs/api/interfaces/StorymapSlideNarration.html +117 -0
  346. package/dist/docs/api/interfaces/StorymapSlideText.html +72 -0
  347. package/dist/docs/api/interfaces/StorymapTilejson.html +105 -0
  348. package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
  349. package/dist/docs/api/interfaces/TextEvents.html +66 -0
  350. package/dist/docs/api/media/es.json +31 -0
  351. package/dist/docs/api/media/events.md +66 -0
  352. package/dist/docs/api/media/migration-from-knightlab.md +399 -0
  353. package/dist/docs/api/media/slideshow.md +109 -0
  354. package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
  355. package/dist/docs/api/modules.html +50 -0
  356. package/dist/docs/api/types/ContentState.html +70 -0
  357. package/dist/docs/api/types/FiredEvent.html +43 -0
  358. package/dist/docs/api/types/ImagereadyKind.html +34 -0
  359. package/dist/docs/api/types/MediaTypeMatch.html +78 -0
  360. package/dist/docs/api/types/SwipeDirection.html +36 -0
  361. package/dist/docs/api/types/TileSourceFactory.html +53 -0
  362. package/dist/docs/authoring.html +306 -0
  363. package/dist/docs/events.html +263 -0
  364. package/dist/docs/iiif.html +738 -69
  365. package/dist/docs/migration.html +360 -11
  366. package/dist/docs/readme.html +149 -17
  367. package/dist/embed/index.html +8 -1
  368. package/dist/examples/font-css-false.json +33 -0
  369. package/dist/examples/issue-242-nonlatin.json +55 -0
  370. package/dist/examples/issue-305-start-at-slide-data.json +96 -0
  371. package/dist/examples/issue-358-iframe.json +57 -0
  372. package/dist/examples/issue-iiif-geo.json +32 -0
  373. package/dist/examples/issue-image-alt.json +23 -0
  374. package/dist/examples/issue-image-region.json +42 -0
  375. package/dist/examples/issue-image-srcset.json +24 -0
  376. package/dist/examples/layers-control.json +60 -0
  377. package/dist/examples/map-area-overlays.json +68 -0
  378. package/dist/examples/map-template-provider.json +36 -0
  379. package/dist/examples/multi-media.json +71 -0
  380. package/dist/examples/no-map.json +44 -0
  381. package/dist/examples-iiif/annotated-image.json +204 -0
  382. package/dist/examples-iiif/bob-boilen.json +11 -5
  383. package/dist/examples-iiif/courbet.json +9 -6
  384. package/dist/examples-iiif/empty.json +8 -4
  385. package/dist/examples-iiif/flickr-drewvigal.json +248 -24
  386. package/dist/examples-iiif/flickr-zach.wise.json +8 -4
  387. package/dist/examples-iiif/font-css-false.json +114 -0
  388. package/dist/examples-iiif/georeferenced-layer-unsupported.json +153 -0
  389. package/dist/examples-iiif/georeferenced-layer.json +318 -0
  390. package/dist/examples-iiif/iiif-wellcome.json +9 -6
  391. package/dist/examples-iiif/instagram-chenpamela.json +381 -36
  392. package/dist/examples-iiif/instagram_brentfoster.json +429 -40
  393. package/dist/examples-iiif/instagram_couch.json +261 -26
  394. package/dist/examples-iiif/instagram_joegermuska.json +441 -41
  395. package/dist/examples-iiif/instagram_matteich.json +261 -26
  396. package/dist/examples-iiif/instagram_mirandamulligan.json +261 -26
  397. package/dist/examples-iiif/instagram_raymondmccrea.json +261 -26
  398. package/dist/examples-iiif/instagram_test.json +261 -26
  399. package/dist/examples-iiif/issue-134-no-lines.json +9 -5
  400. package/dist/examples-iiif/issue-177-color-vars.json +9 -5
  401. package/dist/examples-iiif/issue-211-rtl.json +9 -5
  402. package/dist/examples-iiif/issue-242-nonlatin.json +239 -0
  403. package/dist/examples-iiif/issue-272-facebook.json +9 -5
  404. package/dist/examples-iiif/issue-305-start-at-slide-data.json +305 -0
  405. package/dist/examples-iiif/issue-305-start-at-slide.json +9 -5
  406. package/dist/examples-iiif/issue-341-distance.json +9 -5
  407. package/dist/examples-iiif/issue-358-iframe.json +190 -0
  408. package/dist/examples-iiif/issue-360-juxtapose.json +22 -7
  409. package/dist/examples-iiif/issue-368-null-fields.json +9 -5
  410. package/dist/examples-iiif/issue-375-code-lists.json +9 -5
  411. package/dist/examples-iiif/issue-381-dateline.json +9 -5
  412. package/dist/examples-iiif/issue-405-custom-icon.json +203 -0
  413. package/dist/examples-iiif/issue-425-many-slides.json +9 -5
  414. package/dist/examples-iiif/issue-428-background.json +20 -8
  415. package/dist/examples-iiif/issue-435-googlemaps.json +9 -5
  416. package/dist/examples-iiif/issue-437-documentcloud.json +22 -7
  417. package/dist/examples-iiif/issue-451-webp.json +9 -5
  418. package/dist/examples-iiif/issue-455-audio.json +9 -5
  419. package/dist/examples-iiif/issue-480-shorts.json +9 -5
  420. package/dist/examples-iiif/issue-506-marker-sync.json +9 -5
  421. package/dist/examples-iiif/issue-iiif-geo.json +200 -0
  422. package/dist/examples-iiif/issue-image-alt.json +90 -0
  423. package/dist/examples-iiif/issue-image-region.json +203 -0
  424. package/dist/examples-iiif/issue-image-srcset.json +87 -0
  425. package/dist/examples-iiif/jansteen.json +9 -6
  426. package/dist/examples-iiif/katrina.json +197 -41
  427. package/dist/examples-iiif/layers-control.json +226 -0
  428. package/dist/examples-iiif/map-area-overlays.json +289 -0
  429. package/dist/examples-iiif/map-template-provider.json +133 -0
  430. package/dist/examples-iiif/marktwain.json +255 -58
  431. package/dist/examples-iiif/multi-media.json +237 -0
  432. package/dist/examples-iiif/no-map.json +148 -0
  433. package/dist/examples-iiif/population.json +440 -76
  434. package/dist/examples-iiif/president.json +456 -83
  435. package/dist/examples-iiif/scroll-hint.json +190 -0
  436. package/dist/examples-iiif/seurat.json +113 -22
  437. package/dist/examples-iiif/zoomify-clamp.json +153 -0
  438. package/dist/examples-slideshow/single.json +79 -0
  439. package/dist/examples-slideshow/static/pixel.png +0 -0
  440. package/dist/examples-slideshow/static.json +44 -0
  441. package/dist/harness-multi.html +17 -0
  442. package/dist/harness.html +4 -2
  443. package/dist/index.html +19 -12
  444. package/dist/js/storymap.d.ts +2669 -307
  445. package/dist/js/storymap.js +10820 -6196
  446. package/dist/js/storymap.js.map +1 -1
  447. package/dist/navplace-properties.json +54 -0
  448. package/dist/site.css +54 -36
  449. package/docs/migration-from-knightlab.md +279 -19
  450. package/docs/storymap-as-iiif-manifest.md +633 -136
  451. package/package.json +12 -7
  452. package/schema/storymap.schema.json +733 -80
  453. package/src/animation/easings.ts +8 -1
  454. package/src/animation/tween.ts +186 -0
  455. package/src/core/Browser.ts +6 -5
  456. package/src/core/Load.ts +54 -2
  457. package/src/core/Util.ts +257 -193
  458. package/src/core/mixins.ts +66 -35
  459. package/src/core/viewers.ts +94 -0
  460. package/src/dom/DomEvent.ts +27 -8
  461. package/src/globals.d.ts +8 -0
  462. package/src/language/Language.ts +206 -25
  463. package/src/language/locale/.expected-gaps.json +346 -0
  464. package/src/language/locale/be.json +15 -2
  465. package/src/language/locale/bg.json +15 -2
  466. package/src/language/locale/cs.json +15 -2
  467. package/src/language/locale/de.json +23 -2
  468. package/src/language/locale/el.json +15 -2
  469. package/src/language/locale/en.json +17 -3
  470. package/src/language/locale/es.json +15 -2
  471. package/src/language/locale/et.json +5 -1
  472. package/src/language/locale/fr.json +15 -2
  473. package/src/language/locale/he.json +16 -3
  474. package/src/language/locale/hu.json +15 -2
  475. package/src/language/locale/is.json +5 -1
  476. package/src/language/locale/it.json +15 -2
  477. package/src/language/locale/jp.json +15 -2
  478. package/src/language/locale/ko.json +15 -2
  479. package/src/language/locale/nl.json +15 -2
  480. package/src/language/locale/nn.json +5 -1
  481. package/src/language/locale/no.json +15 -2
  482. package/src/language/locale/pl.json +15 -2
  483. package/src/language/locale/pt.json +15 -2
  484. package/src/language/locale/ru.json +15 -2
  485. package/src/language/locale/sk.json +15 -2
  486. package/src/language/locale/sr.json +5 -1
  487. package/src/language/locale/sv.json +15 -2
  488. package/src/language/locale/tr.json +15 -2
  489. package/src/language/locale/uk.json +15 -2
  490. package/src/language/locale/ur.json +5 -1
  491. package/src/language/locale/zh-cn.json +15 -2
  492. package/src/language/locale/zh-tw.json +15 -2
  493. package/src/main.ts +143 -0
  494. package/src/map/Map.ts +227 -112
  495. package/src/map/MapMarker.ts +44 -8
  496. package/src/map/georeference.ts +325 -0
  497. package/src/map/openlayers/Map.OpenLayers.ts +1601 -263
  498. package/src/map/openlayers/MapMarker.OpenLayers.ts +348 -24
  499. package/src/map/openlayers/zoomifyTiles.ts +1 -4
  500. package/src/map/slideView.ts +149 -0
  501. package/src/map/types.ts +2 -0
  502. package/src/media/EmbedUtil.ts +333 -24
  503. package/src/media/Media.ts +270 -59
  504. package/src/media/embedId.ts +110 -0
  505. package/src/media/types/Audio.ts +9 -82
  506. package/src/media/types/Blockquote.ts +2 -3
  507. package/src/media/types/DailyMotion.ts +5 -10
  508. package/src/media/types/DocumentCloud.ts +6 -35
  509. package/src/media/types/Facebook.ts +3 -4
  510. package/src/media/types/Flickr.ts +23 -12
  511. package/src/media/types/GoogleDoc.ts +3 -4
  512. package/src/media/types/HtmlMedia.ts +233 -0
  513. package/src/media/types/IFrame.ts +3 -4
  514. package/src/media/types/Image.ts +87 -9
  515. package/src/media/types/Juxtapose.ts +14 -5
  516. package/src/media/types/SoundCloud.ts +28 -6
  517. package/src/media/types/Text.ts +46 -21
  518. package/src/media/types/Twitter.ts +35 -20
  519. package/src/media/types/Video.ts +9 -79
  520. package/src/media/types/Vimeo.ts +11 -11
  521. package/src/media/types/Website.ts +23 -13
  522. package/src/media/types/Wikipedia.ts +18 -8
  523. package/src/media/types/YouTube.ts +72 -40
  524. package/src/scss/Typography.scss +16 -18
  525. package/src/scss/VCO.StoryMap.scss +208 -12
  526. package/src/scss/Variables.scss +3 -2
  527. package/src/scss/core/Mixins.scss +5 -63
  528. package/src/scss/core/Reset.scss +5 -18
  529. package/src/scss/fonts/font.amatic-andika.scss +3 -0
  530. package/src/scss/fonts/font.bitter-raleway.scss +6 -0
  531. package/src/scss/fonts/font.cairo.scss +79 -0
  532. package/src/scss/fonts/font.clicker-garamond.scss +5 -0
  533. package/src/scss/fonts/font.dancing-ledger.scss +1 -0
  534. package/src/scss/fonts/font.knightlab.scss +14 -0
  535. package/src/scss/fonts/font.oldstandard.scss +6 -0
  536. package/src/scss/fonts/font.opensans-gentiumbook.scss +5 -0
  537. package/src/scss/fonts/font.playfair-faunaone.scss +2 -0
  538. package/src/scss/fonts/font.playfair.scss +4 -0
  539. package/src/scss/fonts/font.pt.scss +6 -0
  540. package/src/scss/fonts/font.ubuntu.scss +4 -0
  541. package/src/scss/fonts/font.unicaone-vollkorn.scss +5 -0
  542. package/src/scss/map/VCO.Map.scss +1 -1
  543. package/src/scss/map/VCO.MapMarker.scss +114 -8
  544. package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
  545. package/src/scss/media/VCO.Media.scss +14 -17
  546. package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
  547. package/src/scss/media/types/VCO.Media.Text.scss +13 -5
  548. package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
  549. package/src/scss/media/types/VCO.Media.Wikipedia.scss +3 -19
  550. package/src/scss/site/site.scss +55 -36
  551. package/src/scss/slider/VCO.Slide.scss +61 -13
  552. package/src/scss/slider/VCO.SlideNav.scss +34 -24
  553. package/src/scss/slider/VCO.StorySlider.scss +53 -1
  554. package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
  555. package/src/scss/ui/VCO.Consent.scss +63 -13
  556. package/src/scss/ui/VCO.Layers.scss +119 -0
  557. package/src/scss/ui/VCO.MenuBar.Button.scss +143 -14
  558. package/src/scss/ui/VCO.MenuBar.scss +9 -2
  559. package/src/scss/ui/VCO.Message.scss +16 -5
  560. package/src/site/site.ts +2 -1
  561. package/src/slider/Slide.ts +326 -74
  562. package/src/slider/SlideNav.ts +54 -9
  563. package/src/slider/StorySlider.ts +352 -60
  564. package/src/storymap/Consent.ts +429 -82
  565. package/src/storymap/StoryMap.ts +2163 -340
  566. package/src/storymap/content-state.ts +180 -0
  567. package/src/storymap/font-css.ts +72 -0
  568. package/src/storymap/from-slideshow.ts +785 -0
  569. package/src/storymap/iiif-shared.ts +228 -0
  570. package/src/storymap/iiif.ts +1742 -95
  571. package/src/storymap/to-iiif.ts +1008 -0
  572. package/src/storymap/validate.ts +80 -7
  573. package/src/types.ts +591 -29
  574. package/src/ui/LayersControl.ts +302 -0
  575. package/src/ui/MenuBar.ts +325 -61
  576. package/src/ui/Message.ts +44 -2
  577. package/src/ui/Swipable.ts +170 -85
  578. package/dist/assets/demo-D6cTmzn6.js +0 -1
  579. package/dist/assets/harness-BPFlzutp.js +0 -1
  580. package/dist/assets/index-DDsPBPZA.js +0 -1
  581. package/dist/assets/main-Y-MYUSJ4.js +0 -17
  582. package/src/animation/morpheus.d.ts +0 -41
  583. package/src/scss/VCO.StoryMap.Dark.scss +0 -138
  584. package/src/scss/Variables.Dark.scss +0 -56
  585. package/src/scss/media/types/VCO.Media.Instagram.scss +0 -7
  586. package/src/scss/media/types/VCO.Media.Profile.scss +0 -5
  587. package/src/ui/Draggable.ts +0 -374
@@ -0,0 +1,1008 @@
1
+ // Conversion of legacy StoryMapJS documents into IIIF Presentation API 3.0
2
+ // manifests, following the mapping proposed in
3
+ // docs/storymap-as-iiif-manifest.md.
4
+ //
5
+ // This is the writer half of the format. `manifestToStorymapData()` in
6
+ // ./iiif.ts is the reader half, and the two are the only places that know the
7
+ // mapping — scripts/convert-to-iiif.mjs used to hold the writer and now only
8
+ // does the file I/O around this function.
9
+ //
10
+ // Pure by design: no `node:fs`, no DOM, no fetch, no module state. A caller
11
+ // reads a document however it likes, calls storymapToManifest() and serialises
12
+ // the result, which is what the CLI does for public/examples-iiif/.
13
+ import type {
14
+ StorymapData,
15
+ StorymapGeoreference,
16
+ StorymapGeoreferenceBody,
17
+ StorymapOverlayLayer,
18
+ StorymapSlide,
19
+ StorymapSlideLocation,
20
+ } from "../types";
21
+ // Explicit `.ts` extension: scripts/convert-to-iiif.mjs imports this module
22
+ // straight into node --experimental-strip-types, whose ESM resolution does
23
+ // not try extensions. Extensionless (repo style) breaks `npm run convert:iiif`
24
+ // and tests/convert-cli.test.ts catches it.
25
+ import {
26
+ LOCATION_PROPERTIES,
27
+ asRecord,
28
+ isDuplicateNarration,
29
+ isHttpUrl,
30
+ isLonLatBox,
31
+ languageMap,
32
+ writePlaybackTerms,
33
+ } from "./iiif-shared.ts";
34
+
35
+ /**
36
+ * The document {@link storymapToManifest} reads: the `{ "storymap": {...} }`
37
+ * wrapper a `storymap.json` file is, with every member optional.
38
+ *
39
+ * The root is `Partial<StorymapData>` — every legacy key is optional and the
40
+ * files in the wild carry more than the schema documents — or a plain record,
41
+ * which is what a document read straight out of `JSON.parse` is. The wrapper
42
+ * itself is open, because the legacy files also carry a root-level `font_css`
43
+ * and the map configuration service prefers it over the key inside `storymap`.
44
+ */
45
+ export interface StorymapDocument {
46
+ storymap?: Partial<StorymapData> | Record<string, unknown>;
47
+ [key: string]: unknown;
48
+ }
49
+
50
+ /** A IIIF Presentation 3 language map; only the neutral `none` tag is written. */
51
+ export interface StorymapLanguageMap {
52
+ none: string[];
53
+ }
54
+
55
+ /** A `{label, value}` pair — the P3 shape for a caption, credit or rights line. */
56
+ export interface StorymapManifestStatement {
57
+ label: StorymapLanguageMap;
58
+ value: StorymapLanguageMap;
59
+ }
60
+
61
+ /** A IIIF Image API service reference. */
62
+ export interface StorymapManifestImageService {
63
+ id: string;
64
+ type: string;
65
+ profile: string;
66
+ }
67
+
68
+ /**
69
+ * A content resource: a painting body, a thumbnail or a background.
70
+ *
71
+ * One permissive interface rather than a discriminated union, because the
72
+ * emitter only writes these: the shapes it produces are the classified media
73
+ * body, a IIIF `Image`, a `TextualBody` and a `Color`, and they are
74
+ * distinguished by `type` for the reader, not for us.
75
+ */
76
+ export interface StorymapManifestContentResource {
77
+ id?: string;
78
+ type: string;
79
+ format?: string;
80
+ width?: number;
81
+ height?: number;
82
+ value?: string;
83
+ service?: StorymapManifestImageService[];
84
+ }
85
+
86
+ /** A navPlace FeatureCollection: a point for a location, a Polygon for a bbox. */
87
+ export interface StorymapManifestFeatureCollection {
88
+ id: string;
89
+ type: "FeatureCollection";
90
+ features: {
91
+ id: string;
92
+ type: "Feature";
93
+ geometry: {
94
+ type: string;
95
+ coordinates: number[] | number[][] | number[][][];
96
+ };
97
+ /** Marker presentation, described by navplace-properties.json (§3.3). */
98
+ properties: Record<string, unknown>;
99
+ }[];
100
+ }
101
+
102
+ /** A painting annotation: the slide's own media, painted onto its canvas. */
103
+ export interface StorymapManifestAnnotation {
104
+ id: string;
105
+ type: "Annotation";
106
+ motivation: string;
107
+ /** The media caption, as the annotation's own `label` (§2.7). */
108
+ label?: StorymapLanguageMap;
109
+ /** The media credit (§2.7). P3 allows a single object, not an array. */
110
+ requiredStatement?: StorymapManifestStatement;
111
+ /** The media alt text (§2.7). */
112
+ accessibilitySummary?: StorymapLanguageMap;
113
+ body: StorymapManifestContentResource;
114
+ target: string | StorymapManifestSpecificResource;
115
+ /** Playback flags (slideshow audio terms); foreign members, absent when unset. */
116
+ "storymap:loop"?: boolean;
117
+ "storymap:offset"?: number;
118
+ "storymap:play"?: string;
119
+ "storymap:stopOnExit"?: boolean;
120
+ "storymap:stopAllPrevious"?: boolean;
121
+ }
122
+
123
+ /** A painting annotation whose target is a region of a larger image (§2.8). */
124
+ export interface StorymapManifestSpecificResource {
125
+ type: "SpecificResource";
126
+ source: string;
127
+ selector: { type: "ImageApiSelector"; value: string };
128
+ }
129
+
130
+ /** A Georeference Extension annotation for a placed raster (§2.10). */
131
+ export interface StorymapManifestGeoreferencing {
132
+ id: string;
133
+ type: "Annotation";
134
+ motivation: "georeferencing";
135
+ target: {
136
+ id: string;
137
+ type: "Image";
138
+ width: number;
139
+ height: number;
140
+ service: StorymapManifestImageService[];
141
+ };
142
+ requiredStatement?: StorymapManifestStatement;
143
+ body: StorymapGeoreferenceBody;
144
+ }
145
+
146
+ /** A canvas `background`: a painting annotation of the slide's backdrop (§2.6). */
147
+ export interface StorymapManifestBackground {
148
+ id: string;
149
+ type: "Annotation";
150
+ motivation: "painting";
151
+ body: StorymapManifestContentResource | StorymapManifestContentResource[];
152
+ target: string;
153
+ }
154
+
155
+ /** One P3 `Range` over the canvases of a `slide.group` (§3.5). */
156
+ export interface StorymapManifestRange {
157
+ id: string;
158
+ type: "Range";
159
+ label: StorymapLanguageMap;
160
+ items: string[];
161
+ }
162
+
163
+ export interface StorymapManifestCanvas {
164
+ id: string;
165
+ type: "Canvas";
166
+ width: number;
167
+ height: number;
168
+ /** The slide's date, which P3 constrains to a plain string (§2.5). */
169
+ navDate?: string;
170
+ background?: StorymapManifestBackground;
171
+ thumbnail?: StorymapManifestContentResource[];
172
+ items: {
173
+ id: string;
174
+ type: "AnnotationPage";
175
+ items: (StorymapManifestAnnotation | StorymapManifestGeoreferencing)[];
176
+ }[];
177
+ label?: StorymapLanguageMap;
178
+ summary?: StorymapLanguageMap;
179
+ /** An overview slide, which the map's `overview` type carries (§2.4). */
180
+ "storymap:type"?: string;
181
+ "storymap:mediaSrcset"?: unknown;
182
+ "storymap:mediaSizes"?: unknown;
183
+ /** View directives (slideshow rotation/basemap/filter/mask); navPlace carries them when lat/lon exist. */
184
+ "storymap:rotation"?: number;
185
+ "storymap:basemap"?: string;
186
+ "storymap:filter"?: unknown;
187
+ "storymap:mask"?: unknown;
188
+ /** Per-slide autoplay dwell (slideshow slidetimeout) and image overlay. */
189
+ "storymap:slidetimeout"?: number;
190
+ "storymap:imgoverlay"?: unknown;
191
+ /** Provenance identifiers (slideshow manifest/canvas/image ids). */
192
+ "storymap:provenance"?: unknown;
193
+ navPlace?: StorymapManifestFeatureCollection;
194
+ }
195
+
196
+ /**
197
+ * The map configuration service entry, keyed by the `storymap:` terms.
198
+ *
199
+ * Open by necessity: the official validator's Manifest schema is
200
+ * `additionalProperties: false`, which is exactly why the map configuration
201
+ * travels on a service of its own rather than on the Manifest (§3.1).
202
+ */
203
+ export interface StorymapManifestService {
204
+ id: string;
205
+ type: "Service";
206
+ profile: string;
207
+ [term: string]: unknown;
208
+ }
209
+
210
+ export interface StorymapManifest {
211
+ "@context": string[];
212
+ id: string;
213
+ type: "Manifest";
214
+ label: StorymapLanguageMap;
215
+ behavior: string[];
216
+ metadata: { label: StorymapLanguageMap; value: StorymapLanguageMap }[];
217
+ items: StorymapManifestCanvas[];
218
+ structures?: StorymapManifestRange[];
219
+ navPlace?: StorymapManifestFeatureCollection;
220
+ service?: StorymapManifestService[];
221
+ requiredStatement?: StorymapManifestStatement;
222
+ }
223
+
224
+ // The original zoomify tile paths of the legacy fixtures are dead, so image-map
225
+ // storymaps are rewritten to the IIIF reference image used by the repo's IIIF
226
+ // fixtures. The original zoomify definition is preserved as a storymap: term.
227
+ const LAOCOON_ID =
228
+ "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon";
229
+ const LAOCOON_INFO = `${LAOCOON_ID}/info.json`;
230
+ const LAOCOON_IMAGE = `${LAOCOON_ID}/full/max/0/default.jpg`;
231
+ const LAOCOON_WIDTH = 2315;
232
+ const LAOCOON_HEIGHT = 3000;
233
+
234
+ const STORYMAP_CONTEXT = "https://cmahnke.github.io/StoryMapJS/context.json";
235
+ // The navPlace extension's linked data context **must be included before** the
236
+ // Presentation 3 context (navplace §3.1), and every fixture here carries a
237
+ // navPlace, so the order is not negotiable. The two StoryMapJS contexts follow:
238
+ // the properties one describes the terms inside the navPlace Feature bag, which
239
+ // §3.2 allows only via "a registered IIIF API extension or a local linked data
240
+ // context" — there is no extension for marker presentation, so it is local.
241
+ // Frozen, and copied per manifest below: this array used to be handed out by
242
+ // reference, so a host mutating one manifest's @context corrupted every later
243
+ // call in the process.
244
+ const CONTEXTS: readonly string[] = Object.freeze([
245
+ "http://iiif.io/api/extension/navplace/context.json",
246
+ "http://iiif.io/api/presentation/3/context.json",
247
+ `${STORYMAP_CONTEXT.replace(/context\.json$/, "")}navplace-properties.json`,
248
+ STORYMAP_CONTEXT,
249
+ ]);
250
+
251
+ const IMAGE_FORMATS: Record<string, string> = {
252
+ jpg: "image/jpeg",
253
+ jpeg: "image/jpeg",
254
+ png: "image/png",
255
+ gif: "image/gif",
256
+ webp: "image/webp",
257
+ bmp: "image/bmp",
258
+ avif: "image/avif",
259
+ };
260
+
261
+ const VIDEO_HOSTS = ["youtube.com", "youtu.be", "vimeo.com", "dailymotion.com", "vine.co"];
262
+ const SOUND_HOSTS = ["soundcloud.com"];
263
+
264
+ function classifyMediaUrl(url: string): { type: string; format?: string } {
265
+ const ext = url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
266
+ // Own-property check: a bare lookup hits Object.prototype, so a URL
267
+ // ending in `.constructor` (or `.__proto__`) classifies as an Image.
268
+ if (ext && Object.hasOwn(IMAGE_FORMATS, ext)) {
269
+ return { type: "Image", format: IMAGE_FORMATS[ext] };
270
+ }
271
+ let host: string;
272
+ try {
273
+ host = new URL(url).hostname.replace(/^www\./, "");
274
+ } catch {
275
+ return { type: "Text", format: "text/html" };
276
+ }
277
+ if (VIDEO_HOSTS.some((h) => host === h || host.endsWith(`.${h}`))) {
278
+ return { type: "Video" };
279
+ }
280
+ if (SOUND_HOSTS.some((h) => host === h || host.endsWith(`.${h}`))) {
281
+ return { type: "Sound" };
282
+ }
283
+ // Page-embedded media (tweets, Wikipedia, photo pages, ...) - the viewer
284
+ // decides how to render these; the manifest carries a typed reference.
285
+ return { type: "Text", format: "text/html" };
286
+ }
287
+
288
+ /**
289
+ * A value worth writing: neither absent nor the empty string. A type guard, so
290
+ * a declared `string | null` member narrows to the string it is.
291
+ */
292
+ function present<T>(value: T | null | undefined): value is T {
293
+ return value !== undefined && value !== null && value !== "";
294
+ }
295
+
296
+ /** An array of `T`, or an empty one. */
297
+ function asArray<T>(value: unknown): T[] {
298
+ return Array.isArray(value) ? (value as T[]) : [];
299
+ }
300
+
301
+ /**
302
+ * Convert a legacy storymap document to a IIIF Presentation 3.0 manifest.
303
+ *
304
+ * The counterpart of `manifestToStorymapData()`: what that reads, this writes.
305
+ * Nothing but the input is read, so the same document always produces the same
306
+ * manifest — which is what makes regenerating `public/examples-iiif/` a no-op.
307
+ *
308
+ * @param name - The manifest's label, and the stem of its canonical id.
309
+ * @param legacy - The `storymap.json` document (`{ "storymap": {...} }`).
310
+ * @returns The manifest, ready to serialise.
311
+ */
312
+ export function storymapToManifest(name: string, legacy: StorymapDocument): StorymapManifest {
313
+ // The legacy root is open, so the declaration says "a record" where a real
314
+ // document is a `Partial<StorymapData>`; the two are the same thing at
315
+ // runtime and every field below is read the way the reader reads one.
316
+ const storymap = (legacy.storymap ?? {}) as Partial<StorymapData>;
317
+ // A hand-built document can carry anything in this array. A non-object
318
+ // has no text, no media and no location, so it is skipped rather than
319
+ // painted as an empty canvas — or crashing on `slide.text`.
320
+ const slides = asArray<StorymapSlide>(storymap.slides).filter(
321
+ (slide): slide is StorymapSlide => asRecord(slide) !== null,
322
+ );
323
+ const isZoomify = storymap.map_type === "zoomify";
324
+ const isImageMap = isZoomify || storymap.map_type === "iiif";
325
+ const manifestId = `https://example.org/storymap/${name}`;
326
+ // The root-level legacy fields are untyped JSON — `StorymapData` has an open
327
+ // index signature — so the attribution is carried verbatim rather than
328
+ // narrowed, exactly as the writer in scripts/convert-to-iiif.mjs did.
329
+ const iiif = asRecord(storymap.iiif);
330
+ const zoomify = asRecord(storymap.zoomify);
331
+ const attribution = (iiif?.attribution || zoomify?.attribution || "") as string;
332
+
333
+ const canvases = slides.map((slide, i) =>
334
+ buildCanvas(manifestId, i, slide, isImageMap, i === 0 ? asArray(storymap.overlays) : []),
335
+ );
336
+
337
+ const manifest: StorymapManifest = {
338
+ "@context": [...CONTEXTS],
339
+ id: manifestId,
340
+ type: "Manifest",
341
+ label: languageMap(name),
342
+ behavior: ["paged"],
343
+ // P3's `provider` is whoever *published* the content, and the
344
+ // converter does not know that — it only knows what produced the
345
+ // manifest, which is what `metadata` is for. Claiming to be the
346
+ // provider would put "Provider: StoryMapJS" on the credit line of every
347
+ // converted storymap (§3.2).
348
+ metadata: [
349
+ {
350
+ label: languageMap("Generated by"),
351
+ value: languageMap("StoryMapJS"),
352
+ },
353
+ ],
354
+ items: canvases,
355
+ };
356
+ // Story credit (slideshow `creator`/`rights`): structured metadata
357
+ // entries, which the reader keeps in `data.metadata`. The dedicated
358
+ // `provider`/`rights` slots stay untouched — provider names the
359
+ // publisher, which the converter does not know (§3.2).
360
+ const credit = storymap.credit;
361
+ if (credit && typeof credit.creator === "string" && credit.creator !== "") {
362
+ manifest.metadata.push({
363
+ label: languageMap("Creator"),
364
+ value: languageMap(credit.creator),
365
+ });
366
+ }
367
+ if (credit && typeof credit.rights === "string" && credit.rights !== "") {
368
+ manifest.metadata.push({
369
+ label: languageMap("Rights"),
370
+ value: languageMap(credit.rights),
371
+ });
372
+ }
373
+
374
+ // `structures` carries the groups (§3.5). A storymap states a slide's
375
+ // group as a plain string; P3 states it as a Range over the canvases it
376
+ // contains, and the reader turns that back into `slide.group`.
377
+ const structures = buildStructures(manifestId, slides, canvases);
378
+ if (structures) manifest.structures = structures;
379
+
380
+ // A map bbox has no extension term: the interoperable spelling is a
381
+ // navPlace Polygon, which the reader turns back into map_bbox
382
+ // (readNavPlaceBbox). Without this the extent was silently lost on the
383
+ // round trip.
384
+ const bbox = storymap.map_bbox;
385
+ if (isLonLatBox(bbox)) {
386
+ const [west, south, east, north] = bbox;
387
+ manifest.navPlace = {
388
+ id: `${manifestId}/navplace`,
389
+ type: "FeatureCollection",
390
+ features: [
391
+ {
392
+ id: `${manifestId}/navplace/feature/1`,
393
+ type: "Feature",
394
+ geometry: {
395
+ type: "Polygon",
396
+ coordinates: [
397
+ [
398
+ [west, south],
399
+ [east, south],
400
+ [east, north],
401
+ [west, north],
402
+ [west, south],
403
+ ],
404
+ ],
405
+ },
406
+ properties: {},
407
+ },
408
+ ],
409
+ };
410
+ }
411
+
412
+ const config = buildMapConfig(storymap, legacy, isZoomify);
413
+ if (Object.keys(config).length > 0) {
414
+ // The Manifest object is closed for extension terms in the official
415
+ // validator's JSON Schema (see docs "Validator interop"), so the
416
+ // storymap map configuration travels on a dedicated service entry.
417
+ manifest.service = [
418
+ {
419
+ id: `${manifestId}/map-config`,
420
+ type: "Service",
421
+ profile: `${STORYMAP_CONTEXT}/mapconfig`,
422
+ ...config,
423
+ },
424
+ ];
425
+ }
426
+ if (present(attribution)) {
427
+ manifest.requiredStatement = {
428
+ label: languageMap("Attribution"),
429
+ value: languageMap(attribution),
430
+ };
431
+ }
432
+ return manifest;
433
+ }
434
+
435
+ function buildMapConfig(
436
+ storymap: Partial<StorymapData>,
437
+ legacy: StorymapDocument,
438
+ isZoomify: boolean,
439
+ ): Record<string, unknown> {
440
+ const config: Record<string, unknown> = {};
441
+ let mapType = storymap.map_type;
442
+ if (isZoomify) {
443
+ // zoomify support was replaced by the IIIF Image API; the legacy
444
+ // pyramid definition is not carried (nothing reads it)
445
+ mapType = "iiif";
446
+ config["storymap:mapAsImage"] = true;
447
+ }
448
+ if (present(mapType)) {
449
+ // A keyword basemap and a tile URL template are different kinds of
450
+ // thing, and `mapType` used to be both. A keyword the viewer knows how
451
+ // to configure is a `storymap:basemap`; anything with a {z} in it is
452
+ // TileJSON's `tiles`, which is where a tile service belongs (§2.9).
453
+ // The check coerces but the write must too: a non-string map_type
454
+ // would otherwise land verbatim in a slot the reader types as string.
455
+ const mapTypeString = String(mapType);
456
+ if (mapTypeString.includes("{z}")) {
457
+ config.tilejson = { tiles: mapTypeString };
458
+ } else {
459
+ config["storymap:basemap"] = mapTypeString;
460
+ }
461
+ }
462
+ if (storymap.map_as_image !== undefined) {
463
+ config["storymap:mapAsImage"] = storymap.map_as_image;
464
+ }
465
+ if (present(storymap.map_access_token)) {
466
+ config["storymap:mapAccessToken"] = storymap.map_access_token;
467
+ }
468
+ if (present(storymap.map_background_color)) {
469
+ config["storymap:mapBackgroundColor"] = storymap.map_background_color;
470
+ }
471
+ if (storymap.map_center_offset !== undefined) {
472
+ config["storymap:mapCenterOffset"] = storymap.map_center_offset;
473
+ }
474
+ // false is meaningful (no injected theme) and must survive the legacy
475
+ // fallback; everything else keeps the previous `||` semantics.
476
+ const fontCss: unknown =
477
+ legacy.font_css === false || storymap.font_css === false
478
+ ? false
479
+ : legacy.font_css || storymap.font_css;
480
+ if (present(fontCss)) {
481
+ config["storymap:fontCss"] = fontCss;
482
+ }
483
+ if (storymap.call_to_action !== undefined) {
484
+ config["storymap:callToAction"] = storymap.call_to_action;
485
+ }
486
+ if (present(storymap.call_to_action_text)) {
487
+ config["storymap:callToActionText"] = storymap.call_to_action_text;
488
+ }
489
+ if (storymap.start_at_slide !== undefined) {
490
+ config["storymap:startAtSlide"] = storymap.start_at_slide;
491
+ }
492
+ if (present(storymap.language)) {
493
+ config["storymap:language"] = storymap.language;
494
+ }
495
+ if (storymap.calculate_zoom !== undefined) {
496
+ config["storymap:calculateZoom"] = storymap.calculate_zoom;
497
+ }
498
+ if (storymap.line_follows_path !== undefined) {
499
+ config["storymap:lineFollowsPath"] = storymap.line_follows_path;
500
+ }
501
+ if (storymap.show_lines !== undefined) {
502
+ config["storymap:showLines"] = storymap.show_lines;
503
+ }
504
+ if (storymap.show_history_line !== undefined) {
505
+ config["storymap:showHistoryLine"] = storymap.show_history_line;
506
+ }
507
+ if (present(storymap.line_color)) {
508
+ config["storymap:lineColor"] = storymap.line_color;
509
+ }
510
+ if (present(storymap.line_color_inactive)) {
511
+ config["storymap:lineColorInactive"] = storymap.line_color_inactive;
512
+ }
513
+ if (storymap.line_weight !== undefined) {
514
+ config["storymap:lineWeight"] = storymap.line_weight;
515
+ }
516
+ if (storymap.line_opacity !== undefined) {
517
+ config["storymap:lineOpacity"] = storymap.line_opacity;
518
+ }
519
+ if (present(storymap.line_dash)) {
520
+ config["storymap:lineDash"] = storymap.line_dash;
521
+ }
522
+ if (present(storymap.line_join)) {
523
+ config["storymap:lineJoin"] = storymap.line_join;
524
+ }
525
+ if (storymap.use_custom_markers !== undefined) {
526
+ config["storymap:useCustomMarkers"] = storymap.use_custom_markers;
527
+ }
528
+ if (storymap.map_area !== undefined) {
529
+ config["storymap:mapArea"] = storymap.map_area;
530
+ }
531
+ if (isLonLatBox(storymap.overview_extent)) {
532
+ config["storymap:overviewExtent"] = storymap.overview_extent;
533
+ }
534
+ if (storymap.keyboard !== undefined) {
535
+ config["storymap:keyboard"] = storymap.keyboard;
536
+ }
537
+ const overlays = asArray<StorymapOverlayLayer>(storymap.overlays);
538
+ if (overlays.length > 0) {
539
+ // entries without a map_type are dropped on the way back in; an
540
+ // entry carrying both map_type and georeference keeps only the
541
+ // former here, because the georeference is also emitted as an
542
+ // annotation — and the reader would otherwise return two overlays
543
+ // for one source layer.
544
+ config["storymap:overlays"] = overlays
545
+ .filter((entry) => entry && present(entry.map_type))
546
+ .map((entry) => {
547
+ if (!entry.georeference) {
548
+ return entry;
549
+ }
550
+ const { georeference: _dropped, ...rest } = entry;
551
+ return rest;
552
+ });
553
+ }
554
+ return config;
555
+ }
556
+
557
+ function buildCanvas(
558
+ manifestId: string,
559
+ index: number,
560
+ slide: StorymapSlide,
561
+ isImageMap: boolean,
562
+ georeferencedLayers: StorymapOverlayLayer[],
563
+ ): StorymapManifestCanvas {
564
+ const canvasId = `${manifestId}/canvas/${index + 1}`;
565
+ const headline = slide.text?.headline || "";
566
+ const bodyText = slide.text?.text || "";
567
+ const width = isImageMap ? LAOCOON_WIDTH : 1080;
568
+ const height = isImageMap ? LAOCOON_HEIGHT : 1080;
569
+
570
+ const canvas: StorymapManifestCanvas = {
571
+ id: canvasId,
572
+ type: "Canvas",
573
+ height,
574
+ width,
575
+ // P3 spells a slide's date `navDate`, and the official validator
576
+ // constrains it to a plain string — a language map is rejected, and it
577
+ // applies no format constraint at all, so the storymap value is
578
+ // carried verbatim ("Aug 23" and "1790-2010" both pass) (§2.5)
579
+ ...(typeof slide.date === "string" && slide.date !== "" ? { navDate: slide.date } : {}),
580
+ // P3 paints a canvas background with an Annotation referenced from the
581
+ // canvas `background` property (§2.6)
582
+ ...buildBackground(canvasId, slide.background),
583
+ // media.thumb is P3's `thumbnail`, a list of content resources (§3.1)
584
+ ...buildThumbnail(slide),
585
+ items: [
586
+ {
587
+ id: `${canvasId}/annotationpage/1`,
588
+ type: "AnnotationPage",
589
+ items: [
590
+ {
591
+ id: `${canvasId}/annotation/1`,
592
+ type: "Annotation",
593
+ motivation: "painting",
594
+ // P3 defines label / requiredStatement /
595
+ // accessibilitySummary on the Annotation, which is
596
+ // where a caption, a credit and alt text belong for
597
+ // the resource being painted (§2.7)
598
+ ...buildAnnotationPresentation(slide),
599
+ body: buildBody(slide, isImageMap),
600
+ target: buildRegionTarget(canvasId, slide.location?.region) ?? canvasId,
601
+ // Playback flags for audio/video media (slideshow
602
+ // audio terms); absent stays absent.
603
+ ...writePlaybackTerms(slide.media ?? {}),
604
+ },
605
+ // Narration is a supplementing Sound body (V1 of #358):
606
+ // "additional to the painting", with no rendering rules
607
+ // attached — exactly what the dedicated audio element
608
+ // plays. Skipped when it duplicates the painting URL.
609
+ ...buildNarration(canvasId, slide),
610
+ // Extra visible media as further painting annotations (V2
611
+ // of #358): IIIF expresses multiple media per canvas as
612
+ // multiple paintings. The layout (media_layout) has no
613
+ // term and stays viewer-side.
614
+ ...buildExtraPaintings(canvasId, slide),
615
+ // Georeference Extension annotations for the placed rasters
616
+ // (§2.10). The layers are map-wide but an annotation is
617
+ // canvas-scoped, so they are written on the first canvas and
618
+ // the reader collects them from every canvas.
619
+ ...buildGeoreferencing(canvasId, georeferencedLayers),
620
+ ],
621
+ },
622
+ ],
623
+ };
624
+ if (present(headline)) {
625
+ canvas.label = languageMap(headline);
626
+ }
627
+ if (present(bodyText)) {
628
+ canvas.summary = languageMap(bodyText);
629
+ }
630
+
631
+ const storymapTerms = buildCanvasTerms(slide);
632
+ Object.assign(canvas, storymapTerms);
633
+
634
+ const navPlace = buildNavPlace(canvasId, slide);
635
+ if (navPlace) {
636
+ canvas.navPlace = navPlace;
637
+ }
638
+ return canvas;
639
+ }
640
+
641
+ /**
642
+ * A slide's extra visible media as further `painting` annotations (V2 of
643
+ * #358). Each extra reuses the primary's body and presentation builders, so
644
+ * captions, credits and alt text travel the same way; extras without a usable
645
+ * url are skipped, matching the viewer.
646
+ */
647
+ function buildExtraPaintings(canvasId: string, slide: StorymapSlide): StorymapManifestAnnotation[] {
648
+ const extras = Array.isArray(slide.media_extra) ? slide.media_extra : [];
649
+ return extras
650
+ .filter((item) => typeof item?.url === "string" && item.url !== "")
651
+ .map((item, index) => ({
652
+ id: `${canvasId}/annotation/extra-${index + 1}`,
653
+ type: "Annotation",
654
+ motivation: "painting",
655
+ ...buildAnnotationPresentation({ media: item }),
656
+ body: buildBody({ media: item }, false),
657
+ target: canvasId,
658
+ }));
659
+ }
660
+
661
+ /**
662
+ * A slide narration as a `motivation: "supplementing"` annotation (V1 of
663
+ * #358). The body is always a `Sound`: narration plays through the
664
+ * dedicated audio element, which never renders video.
665
+ */
666
+ function buildNarration(canvasId: string, slide: StorymapSlide): StorymapManifestAnnotation[] {
667
+ const url = slide.narration?.url;
668
+ if (typeof url !== "string" || url === "" || isDuplicateNarration([slide.media?.url], url)) {
669
+ return [];
670
+ }
671
+ return [
672
+ {
673
+ id: `${canvasId}/annotation/narration`,
674
+ type: "Annotation",
675
+ motivation: "supplementing",
676
+ body: { id: url, type: "Sound" },
677
+ target: canvasId,
678
+ ...writePlaybackTerms(slide.narration ?? {}),
679
+ },
680
+ ];
681
+ }
682
+
683
+ /**
684
+ * A map's georeferenced rasters as Georeference Extension annotations
685
+ * (§2.10).
686
+ *
687
+ * Per the extension, a layer that is not part of the canvas it ships in — which
688
+ * is every placed raster — carries the image it places in `target` as an
689
+ * embedded resource, and the ground control points in `body` as a
690
+ * FeatureCollection whose features carry `resourceCoords`.
691
+ *
692
+ * The `opacity`, `visible`, `className` and `blendMode` the term could carry
693
+ * have no IIIF vocabulary for a georeferencing annotation, so they are not
694
+ * written; the attribution is, as a `requiredStatement`, which is where P3 puts
695
+ * it.
696
+ */
697
+ function hasGeoreference(
698
+ entry: StorymapOverlayLayer,
699
+ ): entry is StorymapOverlayLayer & { georeference: StorymapGeoreference } {
700
+ // Truthiness alone lets a string width or an Infinity height reach the
701
+ // annotation target, which declares them as numbers — on the one branch
702
+ // the validator whitelists out. Mirror the reader's shape check.
703
+ if (!entry || !entry.georeference) {
704
+ return false;
705
+ }
706
+ const { url, width, height } = entry.georeference as {
707
+ url?: unknown;
708
+ width?: unknown;
709
+ height?: unknown;
710
+ };
711
+ return (
712
+ typeof url === "string" &&
713
+ url !== "" &&
714
+ typeof width === "number" &&
715
+ typeof height === "number" &&
716
+ Number.isFinite(width) &&
717
+ Number.isFinite(height) &&
718
+ width > 0 &&
719
+ height > 0
720
+ );
721
+ }
722
+
723
+ function buildGeoreferencing(
724
+ canvasId: string,
725
+ overlays: StorymapOverlayLayer[],
726
+ ): StorymapManifestGeoreferencing[] {
727
+ const layers = overlays.filter(hasGeoreference);
728
+ return layers.map((entry) => {
729
+ const georeference = entry.georeference;
730
+ return {
731
+ // The index comes from `indexOf` rather than the map's own counter
732
+ // because that is what the script emitted: two entries that are the
733
+ // *same object* share an id. `JSON.parse` never produces that, so
734
+ // the two agree on every real document — the quirk is preserved
735
+ // rather than silently changed by the move into the library.
736
+ id: `${canvasId}/georeferencing/${layers.indexOf(entry) + 1}`,
737
+ type: "Annotation",
738
+ motivation: "georeferencing",
739
+ target: {
740
+ id: georeference.url,
741
+ type: "Image",
742
+ width: georeference.width,
743
+ height: georeference.height,
744
+ service: [{ id: georeference.url, type: "ImageService3", profile: "level2" }],
745
+ },
746
+ ...(present(entry.attribution)
747
+ ? {
748
+ requiredStatement: {
749
+ label: languageMap("Attribution"),
750
+ value: languageMap(entry.attribution),
751
+ },
752
+ }
753
+ : {}),
754
+ body: georeference.body,
755
+ };
756
+ });
757
+ }
758
+
759
+ /**
760
+ * A storymap's `slide.group` values as P3 `structures`: one Range per group,
761
+ * over the canvases it contains (§3.5).
762
+ *
763
+ * Groups come out in first-appearance order, and the slides keep their order
764
+ * within a group — which is what makes a grouped storymap's slide order the
765
+ * concatenation of its groups, rather than the original interleaving. No group
766
+ * anywhere means no `structures` at all.
767
+ */
768
+ function buildStructures(
769
+ manifestId: string,
770
+ slides: StorymapSlide[],
771
+ canvases: StorymapManifestCanvas[],
772
+ ): StorymapManifestRange[] | null {
773
+ const groups: string[] = [];
774
+ const byGroup = new Map<string, string[]>();
775
+ slides.forEach((slide, i) => {
776
+ const group = slide.group;
777
+ if (!present(group)) return;
778
+ if (!byGroup.has(group)) {
779
+ byGroup.set(group, []);
780
+ groups.push(group);
781
+ }
782
+ byGroup.get(group)?.push(canvases[i].id);
783
+ });
784
+ if (groups.length === 0) return null;
785
+ return groups.map((group, i) => ({
786
+ id: `${manifestId}/range/${i + 1}`,
787
+ type: "Range",
788
+ label: languageMap(group),
789
+ items: byGroup.get(group) ?? [],
790
+ }));
791
+ }
792
+
793
+ /**
794
+ * A slide's `media.thumb` as P3's canvas `thumbnail`, which is a list of
795
+ * content resources (§3.1).
796
+ */
797
+ function buildThumbnail(slide: StorymapSlide): Partial<StorymapManifestCanvas> {
798
+ const url = slide.media?.thumb;
799
+ if (!present(url) || typeof url !== "string") return {};
800
+ const { type, format } = classifyMediaUrl(url);
801
+ return { thumbnail: [{ id: url, type, ...(format ? { format } : {}) }] };
802
+ }
803
+
804
+ /**
805
+ * A slide background as P3's canvas `background`: a painting Annotation whose
806
+ * body is the image and/or the colour to paint behind the slide (§2.6). A
807
+ * bare-string background is a colour, as it has always been in storymap-JSON.
808
+ *
809
+ * `background.opacity` is deliberately not carried across. IIIF has no
810
+ * vocabulary for it — Presentation 3 has a `Color` body but nothing to fade a
811
+ * background with, and 4.0's `backgroundColor` is a plain hex value — and the
812
+ * viewer never rendered it, so a term for it would be dead vocabulary.
813
+ */
814
+ function buildBackground(
815
+ canvasId: string,
816
+ background: StorymapSlide["background"],
817
+ ): Partial<StorymapManifestCanvas> {
818
+ if (!background) return {};
819
+ const object = typeof background === "object" ? background : {};
820
+ const url = typeof background === "string" ? null : object.url;
821
+ const color = typeof background === "string" ? background : object.color;
822
+ const bodies: StorymapManifestContentResource[] = [];
823
+ if (present(url)) {
824
+ if (typeof url === "string") {
825
+ const { type, format } = classifyMediaUrl(url);
826
+ bodies.push({ id: url, type, ...(format ? { format } : {}) });
827
+ }
828
+ }
829
+ if (present(color) && typeof color === "string") {
830
+ bodies.push({ type: "Color", value: color });
831
+ }
832
+ if (bodies.length === 0) return {};
833
+ return {
834
+ background: {
835
+ id: `${canvasId}/background`,
836
+ type: "Annotation",
837
+ motivation: "painting",
838
+ body: bodies.length === 1 ? (bodies[0] as StorymapManifestContentResource) : bodies,
839
+ target: canvasId,
840
+ },
841
+ };
842
+ }
843
+
844
+ /**
845
+ * A slide's media caption, credit and alt text as the painting annotation's
846
+ * own P3 properties, replacing the `storymap:mediaCaption` /
847
+ * `mediaCredit` / `mediaAlt` canvas terms (§2.7).
848
+ *
849
+ * `requiredStatement` is a single `{label, value}` object in P3 — the official
850
+ * validator rejects the array form — so the credit becomes a labelled
851
+ * statement, and §2.1's reader turns it back into "Credit: …".
852
+ */
853
+ function buildAnnotationPresentation(slide: StorymapSlide): Partial<StorymapManifestAnnotation> {
854
+ const out: Partial<StorymapManifestAnnotation> = {};
855
+ if (present(slide.media?.caption) && typeof slide.media?.caption === "string") {
856
+ out.label = languageMap(slide.media.caption);
857
+ }
858
+ if (present(slide.media?.credit) && typeof slide.media?.credit === "string") {
859
+ out.requiredStatement = {
860
+ label: languageMap("Credit"),
861
+ value: languageMap(slide.media.credit),
862
+ };
863
+ }
864
+ if (present(slide.media?.alt) && typeof slide.media?.alt === "string") {
865
+ out.accessibilitySummary = languageMap(slide.media.alt);
866
+ }
867
+ return out;
868
+ }
869
+
870
+ function buildBody(slide: StorymapSlide, isImageMap: boolean): StorymapManifestContentResource {
871
+ if (isImageMap) {
872
+ // Image-map canvas: paint the IIIF Image API resource. The service
873
+ // reference lets IIIF-aware viewers request arbitrary resolutions.
874
+ return {
875
+ id: LAOCOON_IMAGE,
876
+ type: "Image",
877
+ format: "image/jpeg",
878
+ width: LAOCOON_WIDTH,
879
+ height: LAOCOON_HEIGHT,
880
+ service: [{ id: LAOCOON_INFO, type: "ImageService3", profile: "level2" }],
881
+ };
882
+ }
883
+ const url = slide.media?.url;
884
+ if (isHttpUrl(url)) {
885
+ const { type, format } = classifyMediaUrl(url);
886
+ const body: StorymapManifestContentResource = { id: url, type };
887
+ if (format) {
888
+ body.format = format;
889
+ }
890
+ return body;
891
+ }
892
+ // A media block with a wrong-typed url is not a text-only slide: painting
893
+ // the caption here would claim it IS the content (and the summary already
894
+ // carries it, so the reader would read it back as media.url). Emit an
895
+ // empty body instead of a plausible lie. An empty string is not
896
+ // wrong-typed — it is the idiomatic "no media" marker — so it keeps the
897
+ // old path, as does a missing url.
898
+ if (url !== undefined && url !== null && url !== "" && typeof url !== "string") {
899
+ return { type: "TextualBody", format: "text/html", value: "" };
900
+ }
901
+ // Text-only slide (or a media value that is not a URL): paint the HTML.
902
+ const html = typeof url === "string" && url !== "" ? url : slide.text?.text || "";
903
+ return {
904
+ type: "TextualBody",
905
+ format: "text/html",
906
+ value: html,
907
+ };
908
+ }
909
+
910
+ function buildCanvasTerms(slide: StorymapSlide): Partial<StorymapManifestCanvas> {
911
+ const terms: Partial<StorymapManifestCanvas> = {};
912
+ if (slide.type === "overview") {
913
+ terms["storymap:type"] = "overview";
914
+ }
915
+ if (present(slide.media?.srcset)) {
916
+ terms["storymap:mediaSrcset"] = slide.media?.srcset;
917
+ }
918
+ if (present(slide.media?.sizes)) {
919
+ terms["storymap:mediaSizes"] = slide.media?.sizes;
920
+ }
921
+ // View directives that also ride the navPlace properties (which only
922
+ // exist when the slide has lat/lon): the canvas terms are the fallback
923
+ // for image-region-only slides, and the reader prefers navPlace.
924
+ if (typeof slide.location?.rotation === "number") {
925
+ terms["storymap:rotation"] = slide.location.rotation;
926
+ }
927
+ if (present(slide.location?.basemap) && typeof slide.location?.basemap === "string") {
928
+ terms["storymap:basemap"] = slide.location.basemap;
929
+ }
930
+ if (present(slide.location?.filter)) {
931
+ terms["storymap:filter"] = slide.location?.filter;
932
+ }
933
+ if (present(slide.location?.mask)) {
934
+ terms["storymap:mask"] = slide.location?.mask;
935
+ }
936
+ if (typeof slide.slidetimeout === "number") {
937
+ terms["storymap:slidetimeout"] = slide.slidetimeout;
938
+ }
939
+ if (present(slide.imgoverlay)) {
940
+ terms["storymap:imgoverlay"] = slide.imgoverlay;
941
+ }
942
+ if (present(slide.provenance)) {
943
+ terms["storymap:provenance"] = slide.provenance;
944
+ }
945
+ return terms;
946
+ }
947
+
948
+ /**
949
+ * A slide's `location.region` as the painting annotation's target, so the
950
+ * region is an Image API selector rather than a term (§2.8).
951
+ *
952
+ * `xywh=pixel:` rather than a bare `xywh=` because the field is documented in
953
+ * image pixels, and the prefix then says so itself instead of implying the
954
+ * canvas size — which matters for a non-image map, where the canvas is a
955
+ * nominal 1080×1080 and the region's image is something else entirely. It is
956
+ * the prefix Media Fragments 1.0 defines for source-image pixels; `image:` is
957
+ * not one of the three it defines.
958
+ */
959
+ function buildRegionTarget(
960
+ canvasId: string,
961
+ region: StorymapSlideLocation["region"],
962
+ ): StorymapManifestSpecificResource | null {
963
+ if (!Array.isArray(region) || region.length !== 4) return null;
964
+ if (!region.every((n) => typeof n === "number" && Number.isFinite(n))) return null;
965
+ return {
966
+ type: "SpecificResource",
967
+ source: canvasId,
968
+ selector: { type: "ImageApiSelector", value: `xywh=pixel:${region.join(",")}` },
969
+ };
970
+ }
971
+
972
+ function buildNavPlace(
973
+ canvasId: string,
974
+ slide: StorymapSlide,
975
+ ): StorymapManifestFeatureCollection | null {
976
+ const location = slide.location || {};
977
+ // typeof narrows for the compiler, isFinite for the runtime: a typeof
978
+ // check alone lets Infinity through, which serialises as null — invalid
979
+ // GeoJSON the reader then drops silently. Same guard as the bbox and
980
+ // region paths.
981
+ const { lat, lon } = location;
982
+ if (
983
+ typeof lat !== "number" ||
984
+ typeof lon !== "number" ||
985
+ !Number.isFinite(lat) ||
986
+ !Number.isFinite(lon)
987
+ ) {
988
+ return null;
989
+ }
990
+ const properties: Record<string, unknown> = {};
991
+ for (const key of LOCATION_PROPERTIES) {
992
+ if (present(location[key])) {
993
+ properties[key] = location[key];
994
+ }
995
+ }
996
+ return {
997
+ id: `${canvasId}/navplace`,
998
+ type: "FeatureCollection",
999
+ features: [
1000
+ {
1001
+ id: `${canvasId}/navplace/feature/1`,
1002
+ type: "Feature",
1003
+ geometry: { type: "Point", coordinates: [lon, lat] },
1004
+ properties,
1005
+ },
1006
+ ],
1007
+ };
1008
+ }