@janga/norna 0.7.6 → 0.7.8

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 (139) hide show
  1. package/README.md +8 -7
  2. package/astro.config.mjs +76 -0
  3. package/bin/norna-cli.mjs +2 -2
  4. package/docs/README.md +3 -3
  5. package/docs/commands.md +7 -6
  6. package/docs/configuration.md +3 -2
  7. package/docs/content.md +202 -91
  8. package/docs/design/site-examples-structure.md +1 -1
  9. package/docs/engine-development.md +3 -3
  10. package/docs/getting-started.md +50 -8
  11. package/docs/images-and-metadata.md +117 -20
  12. package/docs/routes.md +20 -12
  13. package/docs/site-structure.md +9 -5
  14. package/docs/theme.md +12 -8
  15. package/docs/typography.md +9 -10
  16. package/examples/dog-gallery/site/content.md +38 -77
  17. package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -10
  18. package/fixtures/basic/site/content.md +2 -4
  19. package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
  20. package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
  21. package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
  22. package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
  23. package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
  24. package/fixtures/content-model-v2/.astro/data-store.json +1 -0
  25. package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
  26. package/fixtures/content-model-v2/.astro/dev.json +13 -0
  27. package/fixtures/content-model-v2/.astro/dev.log +231 -0
  28. package/fixtures/content-model-v2/.astro/settings.json +5 -0
  29. package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
  30. package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  31. package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
  32. package/fixtures/content-model-v2/dist/guide/index.html +3 -0
  33. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
  34. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
  35. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
  36. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
  37. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
  38. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
  39. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
  40. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
  41. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
  42. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
  43. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
  44. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
  45. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
  46. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
  47. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
  48. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
  49. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
  50. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
  51. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
  52. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
  53. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
  54. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
  55. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
  56. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
  57. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
  58. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
  59. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
  60. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
  61. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
  62. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
  63. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
  64. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
  65. package/fixtures/content-model-v2/dist/index.html +8 -0
  66. package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
  67. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  68. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  69. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  70. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  71. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  72. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  73. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  74. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  75. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  76. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  77. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  78. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  79. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
  80. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  81. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  82. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  83. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  84. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  85. package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
  86. package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  87. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  88. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  89. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  90. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  91. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  92. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  93. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  94. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  95. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  96. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  97. package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
  98. package/fixtures/content-model-v2/site/config.mjs +11 -0
  99. package/fixtures/content-model-v2/site/content.md +84 -0
  100. package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
  101. package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
  102. package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
  103. package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
  104. package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
  105. package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
  106. package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
  107. package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
  108. package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
  109. package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
  110. package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
  111. package/fixtures/content-model-v2/site/theme.md +9 -0
  112. package/package.json +3 -2
  113. package/scripts/deploy-site.mjs +11 -4
  114. package/scripts/dev-local.mjs +6 -0
  115. package/scripts/generate-images.mjs +132 -56
  116. package/scripts/lib/image-dimensions.mjs +26 -0
  117. package/scripts/lib/norna-markdown-blocks.mjs +643 -0
  118. package/scripts/lib/site-content.mjs +31 -29
  119. package/scripts/show-typography.mjs +17 -9
  120. package/scripts/sync-content-sections.mjs +414 -412
  121. package/scripts/test-content-check.mjs +59 -45
  122. package/scripts/test-content-model-v2.mjs +1230 -0
  123. package/scripts/test-engine-commands.mjs +1 -1
  124. package/scripts/test-package-check.mjs +36 -8
  125. package/scripts/test-temporary-visibility.mjs +3 -6
  126. package/src/components/CardList.astro +77 -0
  127. package/src/components/GalleryGrid.astro +22 -12
  128. package/src/components/SiteNavigation.astro +5 -4
  129. package/src/components/SitePage.astro +9 -2
  130. package/src/components/SiteSection.astro +35 -13
  131. package/src/content.config.ts +4 -17
  132. package/src/lib/generatedImages.ts +50 -10
  133. package/src/lib/sectionContent.ts +130 -35
  134. package/src/styles/global.css +187 -0
  135. package/starters/basic/README.md +4 -2
  136. package/starters/basic/site/content.md +9 -11
  137. package/starters/project/README.md +7 -3
  138. package/starters/project/site/content.md +0 -9
  139. package/starters/project/site/routes/010-guide/route-content.md +0 -5
@@ -12,7 +12,9 @@ import {
12
12
  } from './site-paths.mjs';
13
13
  import { parseRouteDirectory } from './route-model.mjs';
14
14
 
15
- export const supportedImageExtensions = new Set(['.jpg', '.jpeg', '.png']);
15
+ export const rasterImageExtensions = new Set(['.jpg', '.jpeg', '.png']);
16
+ export const staticImageExtensions = new Set(['.svg']);
17
+ export const supportedImageExtensions = new Set([...rasterImageExtensions, ...staticImageExtensions]);
16
18
 
17
19
  const h2Regex = /^##\s+.*$/gm;
18
20
  const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
@@ -248,7 +250,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
248
250
  if (knownTopLevelFrontmatterKeys.has(key)) continue;
249
251
 
250
252
  const fix = knownNestedFrontmatterKeys.has(key)
251
- ? `Indent "${key}:" under the section or object it belongs to. For image rows, "gallery:" normally belongs under a "sections" item.`
253
+ ? `Indent "${key}:" under the object it belongs to, or move image content into Norna Markdown blocks.`
252
254
  : `Move "${key}:" under the correct parent key, or remove it if it is not part of the ${fileKind} schema.`;
253
255
 
254
256
  addIssue({
@@ -277,7 +279,6 @@ export const getFrontmatterSections = (frontmatter) => {
277
279
  const sections = [];
278
280
  const lines = frontmatter.split(/\r?\n/);
279
281
  let inSections = false;
280
- let currentSection = null;
281
282
 
282
283
  for (const [index, line] of lines.entries()) {
283
284
  if (/^sections:\s*$/.test(line)) {
@@ -288,30 +289,9 @@ export const getFrontmatterSections = (frontmatter) => {
288
289
  if (!inSections) continue;
289
290
  if (/^[a-zA-Z0-9_-]+:/.test(line)) break;
290
291
 
291
- const sectionMatch = line.match(/^\s{2}-\s+id:\s*([a-z0-9-]+)\s*$/);
292
+ const sectionMatch = line.match(/^\s{2}([a-z0-9-]+):(?:\s+.*)?\s*$/);
292
293
  if (sectionMatch) {
293
- currentSection = { id: sectionMatch[1], images: [], imageReferences: [], carousels: [] };
294
- sections.push(currentSection);
295
- continue;
296
- }
297
-
298
- const carouselMatch = line.match(/^\s{6}-\s+carousel:\s*$/);
299
- if (carouselMatch && currentSection) {
300
- currentSection.carousels.push({ images: [], imageReferences: [], line: index + 1 });
301
- continue;
302
- }
303
-
304
- const imageMatch = line.match(/^\s{6,}-\s+image:\s*["']?([^"'\n]+)["']?\s*$/);
305
- if (imageMatch && currentSection) {
306
- const image = imageMatch[1].trim();
307
- const isCarouselImage = /^\s{10}-\s+image:/.test(line);
308
- const carousel = isCarouselImage ? currentSection.carousels.at(-1) : null;
309
- currentSection.images.push(image);
310
- currentSection.imageReferences.push({ image, line: index + 1 });
311
- if (carousel) {
312
- carousel.images.push(image);
313
- carousel.imageReferences.push({ image, line: index + 1 });
314
- }
294
+ sections.push({ id: sectionMatch[1], line: index + 1 });
315
295
  }
316
296
  }
317
297
 
@@ -367,8 +347,9 @@ export const getBodySections = (body) => {
367
347
  const text = body.slice(start, end).trimEnd();
368
348
  const heading = match[0];
369
349
  const id = getHeadingId(heading);
350
+ const line = body.slice(0, start).split(/\r?\n/).length;
370
351
 
371
- sections.push({ id, heading, text });
352
+ sections.push({ id, heading, line, text });
372
353
  }
373
354
 
374
355
  return { prelude, sections };
@@ -397,7 +378,7 @@ export const getImageFiles = async (directory) => {
397
378
  return files;
398
379
  };
399
380
 
400
- export const getImageIndex = async (contentDir, fail) => {
381
+ export const getImageIndex = async (contentDir, fail, contentLabel = siteImagesLabel) => {
401
382
  const imageFiles = await getImageFiles(contentDir);
402
383
  const imagesByName = new Map();
403
384
 
@@ -406,7 +387,7 @@ export const getImageIndex = async (contentDir, fail) => {
406
387
  const existingPath = imagesByName.get(imageName);
407
388
 
408
389
  if (existingPath) {
409
- fail(`Duplicate image filename "${imageName}" found at ${siteImagesLabel}/${toPosixPath(path.relative(contentDir, existingPath))} and ${siteImagesLabel}/${toPosixPath(path.relative(contentDir, imagePath))}. Image filenames must be globally unique.`);
390
+ fail(`Duplicate image filename "${imageName}" found at ${contentLabel}/${toPosixPath(path.relative(contentDir, existingPath))} and ${contentLabel}/${toPosixPath(path.relative(contentDir, imagePath))}. The filename is ambiguous within this page image root.`);
410
391
  continue;
411
392
  }
412
393
 
@@ -415,3 +396,24 @@ export const getImageIndex = async (contentDir, fail) => {
415
396
 
416
397
  return imagesByName;
417
398
  };
399
+
400
+ export const getImageCandidatesByName = async (contentDir) => {
401
+ const imageFiles = await getImageFiles(contentDir);
402
+ const imagesByName = new Map();
403
+
404
+ for (const imagePath of imageFiles) {
405
+ const imageName = path.basename(imagePath);
406
+
407
+ if (!imagesByName.has(imageName)) {
408
+ imagesByName.set(imageName, []);
409
+ }
410
+
411
+ imagesByName.get(imageName).push(imagePath);
412
+ }
413
+
414
+ for (const candidates of imagesByName.values()) {
415
+ candidates.sort((left, right) => left.localeCompare(right, 'sv'));
416
+ }
417
+
418
+ return imagesByName;
419
+ };
@@ -9,6 +9,7 @@ import {
9
9
  typographyRhythms,
10
10
  } from './lib/typography.mjs';
11
11
  import {
12
+ getBodySections,
12
13
  getContentFiles,
13
14
  readSiteFile,
14
15
  splitSiteFile,
@@ -268,11 +269,11 @@ const formatAnnotatedTypography = (annotated) => ({
268
269
  resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
269
270
  });
270
271
 
271
- const getSectionBlocks = (lines) => {
272
+ const getSectionMetadataBlocks = (lines) => {
272
273
  const sectionsMap = findMap(lines, 'sections');
273
- if (!sectionsMap) return [];
274
+ if (!sectionsMap) return new Map();
274
275
 
275
- const sections = [];
276
+ const sections = new Map();
276
277
  let current = null;
277
278
 
278
279
  for (let index = sectionsMap.index + 1; index < lines.length; index += 1) {
@@ -282,15 +283,16 @@ const getSectionBlocks = (lines) => {
282
283
  const indent = countIndent(line);
283
284
  if (indent <= sectionsMap.indent) break;
284
285
 
285
- const sectionMatch = line.match(/^\s{2}-\s+id:\s*([a-z0-9-]+)\s*$/);
286
+ const sectionMatch = line.match(/^\s{2}([a-z0-9-]+):(?:\s+.*)?\s*$/);
286
287
  if (sectionMatch) {
287
288
  if (current) current.end = index;
288
289
  current = { id: sectionMatch[1], start: index, end: lines.length };
289
- sections.push(current);
290
+ sections.set(current.id, current);
290
291
  }
291
292
  }
292
293
 
293
- const finalSection = sections.at(-1);
294
+ const sectionList = Array.from(sections.values());
295
+ const finalSection = sectionList.at(-1);
294
296
  if (finalSection) {
295
297
  const afterSections = lines.findIndex((line, index) => (
296
298
  index > finalSection.start &&
@@ -328,7 +330,7 @@ const readThemeTypography = async () => {
328
330
  };
329
331
 
330
332
  const readPageTypography = async (contentFile, themeTypography) => {
331
- const { frontmatter, frontmatterBody } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
333
+ const { frontmatter, frontmatterBody, body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
332
334
  const indentationIssues = [];
333
335
  validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
334
336
  validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
@@ -345,8 +347,14 @@ const readPageTypography = async (contentFile, themeTypography) => {
345
347
  ? findMap(lines, 'typography', pagePresentation.index + 1, pagePresentation.nextIndex)?.value
346
348
  : null;
347
349
  const pageTypography = resolveAnnotatedTypographyOverride(themeTypography, pageTypographyConfig ?? undefined, contentFile.contentLabel);
348
- const sections = getSectionBlocks(lines).map((section) => {
349
- const presentation = findMap(lines, 'presentation', section.start + 1, section.end);
350
+ const sectionMetadataBlocks = getSectionMetadataBlocks(lines);
351
+ const sections = getBodySections(body).sections
352
+ .filter((section) => section.id)
353
+ .map((section) => {
354
+ const metadata = sectionMetadataBlocks.get(section.id);
355
+ const presentation = metadata
356
+ ? findMap(lines, 'presentation', metadata.start + 1, metadata.end)
357
+ : null;
350
358
  const typography = presentation
351
359
  ? findMap(lines, 'typography', presentation.index + 1, presentation.nextIndex)?.value
352
360
  : null;