@heroiclands/package-build 8.1.0 → 10.0.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 (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -150,10 +150,7 @@ export function registryOf(src, name) {
150
150
  }
151
151
  if (ts.isObjectLiteralExpression(init)) {
152
152
  for (const p of init.properties) {
153
- if (
154
- ts.isPropertyAssignment(p) &&
155
- ts.isIdentifier(p.initializer)
156
- ) {
153
+ if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.initializer)) {
157
154
  out.set(propName(p.name), p.initializer.text);
158
155
  }
159
156
  }
@@ -216,10 +213,7 @@ function resolveSpecifier(fromDir, spec, aliases) {
216
213
  }
217
214
  if (!base) return null;
218
215
  for (const ext of EXTENSIONS) {
219
- for (const candidate of [
220
- `${base}${ext}`,
221
- path.join(base, `index${ext}`),
222
- ]) {
216
+ for (const candidate of [`${base}${ext}`, path.join(base, `index${ext}`)]) {
223
217
  if (fs.existsSync(candidate)) return candidate;
224
218
  }
225
219
  }
@@ -243,11 +237,7 @@ function importSourceOf(src, name, aliases) {
243
237
  if (!bindings.elements.some((e) => e.name.text === name)) return;
244
238
  const spec = node.moduleSpecifier;
245
239
  if (!ts.isStringLiteral(spec)) return;
246
- found = resolveSpecifier(
247
- path.dirname(src.fileName),
248
- spec.text,
249
- aliases,
250
- );
240
+ found = resolveSpecifier(path.dirname(src.fileName), spec.text, aliases);
251
241
  });
252
242
  return found;
253
243
  }
@@ -286,9 +276,7 @@ function locateClass(fromFile, className, aliases, cache) {
286
276
  if (!imported) return null;
287
277
  const importedSrc = parse(imported, cache);
288
278
  const importedDecl = classDeclIn(importedSrc, className);
289
- return importedDecl ?
290
- { file: imported, src: importedSrc, decl: importedDecl }
291
- : null;
279
+ return importedDecl ? { file: imported, src: importedSrc, decl: importedDecl } : null;
292
280
  }
293
281
 
294
282
  /**
@@ -303,8 +291,7 @@ function superClassOf(decl) {
303
291
  for (const clause of decl.heritageClauses ?? []) {
304
292
  if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
305
293
  const [type] = clause.types;
306
- if (type && ts.isIdentifier(type.expression))
307
- return type.expression.text;
294
+ if (type && ts.isIdentifier(type.expression)) return type.expression.text;
308
295
  }
309
296
  return null;
310
297
  }
@@ -350,9 +337,8 @@ function literalReturnedBy(src, fnName) {
350
337
  if (isTarget) {
351
338
  const body =
352
339
  ts.isFunctionDeclaration(node) ? node.body
353
- : node.initializer && ts.isArrowFunction(node.initializer) ?
354
- node.initializer.body
355
- : null;
340
+ : node.initializer && ts.isArrowFunction(node.initializer) ? node.initializer.body
341
+ : null;
356
342
  if (body) literal = returnedLiteral(body);
357
343
  }
358
344
  ts.forEachChild(node, visit);
@@ -404,14 +390,10 @@ function isObjectAssign(expr) {
404
390
  */
405
391
  function schemaContributions(src, decl) {
406
392
  const method = decl.members.find(
407
- (m) =>
408
- ts.isMethodDeclaration(m) &&
409
- propName(m.name) === "defineSchema" &&
410
- m.body,
393
+ (m) => ts.isMethodDeclaration(m) && propName(m.name) === "defineSchema" && m.body,
411
394
  );
412
395
  // No `defineSchema()` — the whole schema is the parent's.
413
- if (!method)
414
- return { literals: [], edges: [{ kind: "super", name: null }] };
396
+ if (!method) return { literals: [], edges: [{ kind: "super", name: null }] };
415
397
 
416
398
  const direct = returnedLiteral(method.body);
417
399
  if (direct) return { literals: [direct], edges: [] };
@@ -568,12 +550,7 @@ export function fieldsForClass({ file, className, aliases, cache, rootDir }) {
568
550
  const importedSrc = parse(from, cache);
569
551
  const importedLit = literalReturnedBy(importedSrc, edge.name);
570
552
  if (importedLit) {
571
- walkLiteral(
572
- { file: from, src: importedSrc },
573
- importedLit,
574
- inherited,
575
- superName,
576
- );
553
+ walkLiteral({ file: from, src: importedSrc }, importedLit, inherited, superName);
577
554
  }
578
555
  return;
579
556
  }
@@ -616,12 +593,7 @@ export function fieldsForClass({ file, className, aliases, cache, rootDir }) {
616
593
  * @param {string} opts.version - The package version.
617
594
  * @returns {object} The artifact `schema-check.mjs` reads.
618
595
  */
619
- export function buildSchemaArtifact({
620
- rootDir,
621
- registries,
622
- packageId,
623
- version,
624
- }) {
596
+ export function buildSchemaArtifact({ rootDir, registries, packageId, version }) {
625
597
  const aliases = pathAliases(rootDir);
626
598
  const cache = new Map();
627
599
  const documents = {};
@@ -56,10 +56,7 @@ import { expandContentTables } from "./content-tables.mjs";
56
56
  import { buildSiteIndex, wikiContext } from "./site-index.mjs";
57
57
  import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
58
58
  import { loadForeignManifests, manifestsComplete } from "./kb-manifest.mjs";
59
- import {
60
- formatUnaddressableFinding,
61
- unaddressableForeignPackages,
62
- } from "./foreign-manifests.mjs";
59
+ import { formatUnaddressableFinding, unaddressableForeignPackages } from "./foreign-manifests.mjs";
63
60
  import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
64
61
  import { loadPackConfig } from "./pack-config.mjs";
65
62
  import { searchableFrontmatter } from "./note-package.mjs";
@@ -192,10 +189,7 @@ export function collectContentPages(contentBase, ctx) {
192
189
  // authoring folder, for grouped landings.
193
190
  folder: path.basename(path.dirname(file)),
194
191
  sec,
195
- url:
196
- isReadme ?
197
- `${ctx.mount}${sec}/`
198
- : `${ctx.mount}${sec}/${slug}/`,
192
+ url: isReadme ? `${ctx.mount}${sec}/` : `${ctx.mount}${sec}/${slug}/`,
199
193
  isReadme,
200
194
  });
201
195
  }
@@ -229,12 +223,10 @@ export function collectTreePages(tree, ctx) {
229
223
  const rel = path.relative(tree.from, file).replace(/\\/g, "/");
230
224
  const base = path.basename(rel);
231
225
  const isReadme = base.toLowerCase() === "readme.md";
232
- const sec = fm.category ?? tree.section;
226
+ const sec = fm.subType ?? tree.section;
233
227
  const h1 = /^#\s+(.+?)\s*$/m.exec(body);
234
- const h1Title =
235
- h1 ? h1[1].replace(/\{@link\s+[^}]*\}/g, "").trim() : null;
236
- const name =
237
- fm.name?.full ?? fm.title ?? h1Title ?? path.basename(base, ".md");
228
+ const h1Title = h1 ? h1[1].replace(/\{@link\s+[^}]*\}/g, "").trim() : null;
229
+ const name = fm.name?.full ?? fm.title ?? h1Title ?? path.basename(base, ".md");
238
230
  const slug = fm.slug ?? slugify(path.basename(base, ".md"));
239
231
  const relNoExt = rel.slice(0, -3).toLowerCase();
240
232
  const dir = path.posix.dirname(relNoExt);
@@ -562,9 +554,7 @@ export function pageDestination(page) {
562
554
  : path.join(page.sec, `${page.slug}.md`);
563
555
  }
564
556
  const rel =
565
- page.isReadme ?
566
- path.posix.join(path.posix.dirname(page.rel), "_index.md")
567
- : page.rel;
557
+ page.isReadme ? path.posix.join(path.posix.dirname(page.rel), "_index.md") : page.rel;
568
558
  return path.join(page.sec, rel);
569
559
  }
570
560
 
@@ -640,10 +630,7 @@ export function renderPages(pages, options) {
640
630
  const data = pageFrontmatter(page, { readmeSections, decorate });
641
631
  const dest = path.join(outRoot, pageDestination(page));
642
632
  fs.mkdirSync(path.dirname(dest), { recursive: true });
643
- fs.writeFileSync(
644
- dest,
645
- matter.stringify(protectCode(body, resolve), data),
646
- );
633
+ fs.writeFileSync(dest, matter.stringify(protectCode(body, resolve), data));
647
634
  byKind[page.kind] = (byKind[page.kind] ?? 0) + 1;
648
635
  }
649
636
 
@@ -676,10 +663,7 @@ export function renderPages(pages, options) {
676
663
  * @param {object} options - `{ sections, landing, sectionTitle }`.
677
664
  * @returns {number} How many landings were written.
678
665
  */
679
- export function writeSectionLandings(
680
- outRoot,
681
- { sections = {}, landing, sectionTitle },
682
- ) {
666
+ export function writeSectionLandings(outRoot, { sections = {}, landing, sectionTitle }) {
683
667
  let written = 0;
684
668
 
685
669
  // The mount's own landing carries a `type` of its own. Hugo's template
@@ -689,10 +673,7 @@ export function writeSectionLandings(
689
673
  // template out of the path where it could be inherited.
690
674
  if (landing) {
691
675
  fs.mkdirSync(outRoot, { recursive: true });
692
- fs.writeFileSync(
693
- path.join(outRoot, "_index.md"),
694
- matter.stringify("", landing),
695
- );
676
+ fs.writeFileSync(path.join(outRoot, "_index.md"), matter.stringify("", landing));
696
677
  written += 1;
697
678
  }
698
679
 
@@ -713,10 +694,7 @@ export function writeSectionLandings(
713
694
  if (!entry.isDirectory()) continue;
714
695
  const index = path.join(outRoot, entry.name, "_index.md");
715
696
  if (fs.existsSync(index)) continue;
716
- fs.writeFileSync(
717
- index,
718
- matter.stringify("", { title: sectionTitle(entry.name) }),
719
- );
697
+ fs.writeFileSync(index, matter.stringify("", { title: sectionTitle(entry.name) }));
720
698
  written += 1;
721
699
  }
722
700
  return written;
@@ -859,19 +837,16 @@ export function buildSite({ config, outRoot } = {}) {
859
837
  const outBase = resolveOutputRoot(resolved.rootDir, site.out);
860
838
  const out =
861
839
  outRoot ? path.resolve(outRoot)
862
- : publishesContent ?
863
- path.join(outBase, scheme.prefix.replace(/\/$/, ""))
840
+ : publishesContent ? path.join(outBase, scheme.prefix.replace(/\/$/, ""))
864
841
  // Homepage-only has no content mount, so the package's root *is*
865
842
  // the output root and `--out` redirects the whole of it.
866
- : outBase;
843
+ : outBase;
867
844
  // The homepage publishes at `/<contentPackage>/`, which is the package's
868
845
  // own root — one level above the content mount, and the same directory in
869
846
  // homepage-only mode.
870
847
  const homeRoot = publishesContent ? outBase : out;
871
848
 
872
- const packages = new Set(
873
- site.packages.length ? site.packages : [resolved.contentPackage],
874
- );
849
+ const packages = new Set(site.packages.length ? site.packages : [resolved.contentPackage]);
875
850
 
876
851
  const ctx = {
877
852
  packages,
@@ -54,6 +54,9 @@ import path from "node:path";
54
54
  import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
55
55
  import { hasDocEntry } from "./item-docs.mjs";
56
56
  import { contentPackage } from "./content-package.mjs";
57
+ // The alias namespace: what a page may be called, and what happens when two
58
+ // pages of one type claim one name (#131).
59
+ import { aliasesOf, indexAliases } from "./alias-index.mjs";
57
60
 
58
61
  /**
59
62
  * One page the site will publish, as the index needs to see it.
@@ -152,10 +155,7 @@ function mergeForeign(index, foreignIndex) {
152
155
  const parts = readCanonicalKey(key);
153
156
  if (!parts) continue;
154
157
  const shortKey = `${parts.type}/${parts.shortcode}`;
155
- if (
156
- short.has(shortKey) &&
157
- short.get(shortKey).package !== value.package
158
- ) {
158
+ if (short.has(shortKey) && short.get(shortKey).package !== value.package) {
159
159
  ambiguous.add(shortKey);
160
160
  } else {
161
161
  short.set(shortKey, value);
@@ -183,11 +183,17 @@ function mergeForeign(index, foreignIndex) {
183
183
  export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
184
184
  const index = new Map();
185
185
  const ambiguous = new Set();
186
- const typeAlias = new Map();
187
- const typeCollide = new Set();
186
+ /** One entry per page, fed to {@link indexAliases} in a pass of its own. */
187
+ const aliasEntries = [];
188
188
  const contentTypes = new Set();
189
189
  const sections = new Set();
190
190
  const refIndex = new Map();
191
+ // Every package an address may name: this build's own, plus every one a
192
+ // vendored manifest speaks for. Without it `readQualifier` cannot see the
193
+ // leading package segment of a canonical address, and `kethira-place-x`
194
+ // reads as the unknown type `kethira` (#131).
195
+ const ownPackage = contentPackage();
196
+ const packages = new Set(ownPackage ? [ownPackage] : []);
191
197
 
192
198
  // `section/slug` is unique by construction; the rest are fallbacks.
193
199
  for (const e of entries) {
@@ -206,6 +212,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
206
212
  // without it the link reads as prose and silently loses its href.
207
213
  for (const value of foreignIndex.values()) {
208
214
  if (value.type) contentTypes.add(value.type);
215
+ if (value.package) packages.add(value.package);
209
216
  }
210
217
 
211
218
  // Merged *before* the local type-scoped pass below, so a local page always
@@ -242,10 +249,8 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
242
249
  // The page's package is the configured one — the site collection
243
250
  // resolves it and records it as `pkg`. Never read out of
244
251
  // frontmatter: `package:` is retired (#56).
245
- index.set(
246
- canonicalKey(e.pkg ?? contentPackage(), type, shortcode),
247
- value,
248
- );
252
+ index.set(canonicalKey(e.pkg ?? ownPackage, type, shortcode), value);
253
+ if (e.pkg) packages.add(e.pkg);
249
254
  // In Foundry an item and its documentation are two documents, so
250
255
  // `skill/wpnc` and `docskill/wpnc` are two UUIDs (#1362). Here the
251
256
  // item note renders as one page which *is* its documentation, so
@@ -259,33 +264,38 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
259
264
  }
260
265
  }
261
266
 
262
- const aliases = [
263
- ...(Array.isArray(e.fm.aliases) ? e.fm.aliases : []),
264
- ...(Array.isArray(e.fm.name?.aliases) ? e.fm.name.aliases : []),
265
- e.name,
266
- path.basename(e.base, ".md").replace(/_/g, " "),
267
- ].filter((a) => typeof a === "string" && a);
268
-
269
- for (const alias of aliases) {
270
- const key = `${type}|${alias}`.toLowerCase();
271
- if (typeCollide.has(key)) continue;
272
- const cur = typeAlias.get(key);
273
- if (cur && cur.url !== value.url) {
274
- typeAlias.delete(key);
275
- typeCollide.add(key);
276
- } else if (!cur) {
277
- typeAlias.set(key, value);
278
- }
279
- }
267
+ // The alias sources are the shared ones — `aliases`, `name.aliases`
268
+ // and `name.full`, and deliberately **not** the filename (#131) — plus
269
+ // this page's display name, which is what `name.full` becomes on the
270
+ // site and is carried here already resolved.
271
+ aliasEntries.push({
272
+ type,
273
+ aliases: [...aliasesOf(e.fm), e.name],
274
+ value,
275
+ });
280
276
  }
281
277
 
278
+ // Built in one pass at the end, by the shared rule, so a collision is a
279
+ // reportable fact rather than a silently deleted key. Two pages of one
280
+ // type sharing an alias resolve to neither, and `aliasCollisions` names
281
+ // every claimant — the citing page is innocent (#13, #131).
282
+ const {
283
+ byKey: typeAlias,
284
+ collisions: aliasCollisions,
285
+ claims: aliasClaims,
286
+ } = indexAliases(aliasEntries, { same: (a, b) => a.url === b.url });
287
+ const typeCollide = new Set(aliasCollisions.map((c) => c.key));
288
+
282
289
  return {
283
290
  index,
284
291
  ambiguous,
285
292
  typeAlias,
286
293
  typeCollide,
294
+ aliasCollisions,
295
+ aliasClaims,
287
296
  contentTypes,
288
297
  sections,
298
+ packages,
289
299
  refIndex,
290
300
  conflicts,
291
301
  };
@@ -315,13 +325,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
315
325
  */
316
326
  export function wikiContext(
317
327
  built,
318
- {
319
- src,
320
- type = null,
321
- errors,
322
- foreignIndex = new Map(),
323
- manifestsComplete = true,
324
- },
328
+ { src, type = null, errors, foreignIndex = new Map(), manifestsComplete = true },
325
329
  ) {
326
330
  return {
327
331
  index: built.index,
@@ -332,6 +336,7 @@ export function wikiContext(
332
336
  typeAlias: built.typeAlias,
333
337
  typeCollide: built.typeCollide,
334
338
  contentTypes: built.contentTypes,
339
+ packages: built.packages,
335
340
  type,
336
341
  errors,
337
342
  src,