@purview-dev/astroc4 1.0.0-prerelease.6 → 1.0.0-prerelease.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.
package/README.md CHANGED
@@ -145,6 +145,7 @@ reload in `astro dev` like any other.
145
145
  | `projects` | `{}` | Named LikeC4 project folders, relative to the site root or absolute. See above. |
146
146
  | `vitePlugin` | `{}` | Passed to `LikeC4VitePlugin`. Setting `workspace` or `languageServices` takes discovery over: `projects` is then ignored (with a warning). |
147
147
  | `fences` | `true` | Turn ` ```likec4 ` fences into diagrams. |
148
+ | `validation` | `'strict'` | Whether a fence that cannot be drawn (bad DSL, missing view, bad `project`, unresolved `file=`) fails the build or only warns. `'relaxed'` warns and leaves it as a highlighted code block; `{ default, collections, relaxed: [globs] }` scopes it by collection or path. See [Fences](docs/wiki/guide/Fences.md#partial-and-invalid-examples). |
148
149
  | `highlighting` | `true` | Highlight LikeC4 source. Starlight only — a plain Astro site passes `likec4Language` to Expressive Code. See below. |
149
150
 
150
151
  ### Syntax highlighting
package/dist/fences.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MdastPluginEntry } from 'satteri';
2
+ import { type ValidationOptions } from './validation.js';
2
3
  type LikeC4Api = typeof import('likec4').LikeC4;
3
4
  /**
4
5
  * Finishes loading LikeC4 before the first page renders, so the load cannot race
@@ -39,8 +40,19 @@ export interface FenceOptions {
39
40
  * `project` is checked at build time; without it, only the browser can.
40
41
  */
41
42
  projects?: string[];
43
+ /**
44
+ * How a fence that cannot be turned into a diagram is treated. `strict` (the
45
+ * default) fails the build and `astro check`; `relaxed` logs a warning and
46
+ * leaves the fence as a highlighted code block. Relaxing can be scoped by
47
+ * page path glob or content collection. See `./validation.ts`.
48
+ */
49
+ validation?: ValidationOptions;
50
+ /** The site's content directory, so a page's collection can be resolved. */
51
+ contentDir?: string;
52
+ /** Called with the reason for each fence `relaxed` validation leaves as a code block. */
53
+ onInvalid?: (message: string) => void;
42
54
  }
43
- export declare function likec4Fences({ root, projects }?: FenceOptions): MdastPluginEntry;
55
+ export declare function likec4Fences({ root, projects, validation, contentDir, onInvalid }?: FenceOptions): MdastPluginEntry;
44
56
  export interface FencePlaceholder {
45
57
  viewId: string;
46
58
  /** The laid-out model of an inline or `file=` fence; absent for a project view. */
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { AstroIntegration } from 'astro';
3
3
  import { missingOptimizedDependencies } from './integration.js';
4
4
  import type { LikeC4IntegrationOptions } from './integration.js';
5
5
  export { missingOptimizedDependencies };
6
+ export type { ValidationMode, ValidationOptions, ValidationScope } from './validation.js';
6
7
  export interface LikeC4HighlightingOptions {
7
8
  /** Branch, tag or commit SHA of `likec4/likec4`. Defaults to `main`; also `ASTROC4_REF`. */
8
9
  ref?: string;
package/dist/index.js CHANGED
@@ -37,6 +37,45 @@ var translations = {
37
37
  }
38
38
  };
39
39
 
40
+ // src/validation.ts
41
+ function globToRegExp(pattern) {
42
+ let body = "";
43
+ for (let index = 0;index < pattern.length; index += 1) {
44
+ const char = pattern[index];
45
+ if (char === "*") {
46
+ if (pattern[index + 1] === "*") {
47
+ body += ".*";
48
+ index += 1;
49
+ } else {
50
+ body += "[^/]*";
51
+ }
52
+ } else if (char === "?") {
53
+ body += "[^/]";
54
+ } else if (".+^${}()|[]\\".includes(char)) {
55
+ body += `\\${char}`;
56
+ } else {
57
+ body += char;
58
+ }
59
+ }
60
+ return new RegExp(`^(?:.*/)?${body}$`);
61
+ }
62
+ var toSlashes = (value) => value.replaceAll("\\", "/");
63
+ function validationModeFor(options, relativePath, collection) {
64
+ if (options === undefined)
65
+ return "strict";
66
+ if (typeof options === "string")
67
+ return options;
68
+ const fallback = options.default ?? "strict";
69
+ const collections = options.collections === undefined ? [] : [options.collections].flat();
70
+ if (collection !== undefined && collections.includes(collection))
71
+ return "relaxed";
72
+ if (relativePath === undefined)
73
+ return fallback;
74
+ const patterns = options.relaxed === undefined ? [] : [options.relaxed].flat();
75
+ const path = toSlashes(relativePath);
76
+ return patterns.some((pattern) => globToRegExp(pattern).test(path)) ? "relaxed" : fallback;
77
+ }
78
+
40
79
  // src/fences.ts
41
80
  var require2 = createRequire(import.meta.url);
42
81
  var likec4ApiPromise;
@@ -56,6 +95,7 @@ var IMPORTS = {
56
95
  [PROJECT_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4View.astro`
57
96
  };
58
97
  var STRINGS = translations.en;
98
+ var RELAXED_NOTE = "Left as a highlighted code block because this page is validated in `relaxed` mode.";
59
99
  var layoutCache = new Map;
60
100
  function parseFenceMeta(meta) {
61
101
  const options = {};
@@ -123,7 +163,23 @@ function describe(fileURL) {
123
163
  return String(fileURL);
124
164
  }
125
165
  }
126
- function likec4Fences({ root, projects } = {}) {
166
+ function pagePath(fileURL, root) {
167
+ if (!fileURL || !root)
168
+ return;
169
+ try {
170
+ return relative(resolve(root), fileURLToPath(fileURL));
171
+ } catch {
172
+ return;
173
+ }
174
+ }
175
+ function collectionOf(fileURL, contentDir) {
176
+ const within = pagePath(fileURL, contentDir);
177
+ if (!within || within.startsWith(".."))
178
+ return;
179
+ const [first, ...rest] = within.split(/[\\/]/);
180
+ return rest.length > 0 ? first : undefined;
181
+ }
182
+ function likec4Fences({ root, projects, validation, contentDir, onInvalid } = {}) {
127
183
  return (factory) => {
128
184
  if (!factory.source.includes("```" + FENCE_LANG))
129
185
  return null;
@@ -134,9 +190,21 @@ function likec4Fences({ root, projects } = {}) {
134
190
  async code(node, ctx) {
135
191
  if (node.lang !== FENCE_LANG)
136
192
  return;
193
+ const mode = validationModeFor(validation, pagePath(ctx.fileURL, root), collectionOf(ctx.fileURL, contentDir));
194
+ const fail = (reason) => {
195
+ if (mode === "strict")
196
+ throw new Error(reason);
197
+ onInvalid?.(`${reason} ${RELAXED_NOTE}`);
198
+ };
137
199
  const where = `${describe(ctx.fileURL)}: \`\`\`${FENCE_LANG} fence`;
138
200
  const options = parseFenceMeta(node.meta);
139
- const source = options.file ? await readFenceFile(options.file, ctx.fileURL, root, where) : (node.value ?? "").trim();
201
+ let source;
202
+ try {
203
+ source = options.file ? await readFenceFile(options.file, ctx.fileURL, root, where) : (node.value ?? "").trim();
204
+ } catch (error) {
205
+ fail(error instanceof Error ? error.message : String(error));
206
+ return;
207
+ }
140
208
  const common = {
141
209
  title: options.title,
142
210
  description: options.description,
@@ -146,9 +214,14 @@ function likec4Fences({ root, projects } = {}) {
146
214
  };
147
215
  if (source.length === 0) {
148
216
  if (!options.view) {
149
- throw new Error(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
217
+ fail(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
218
+ return;
219
+ }
220
+ const projectFailure = projectError(options.project, projects, where);
221
+ if (projectFailure !== undefined) {
222
+ fail(projectFailure);
223
+ return;
150
224
  }
151
- checkProject(options.project, projects, where);
152
225
  if (markdown) {
153
226
  ctx.replaceNode(node, {
154
227
  raw: fencePlaceholder({ ...placeholderOptions(options), viewId: options.view, project: options.project }),
@@ -169,11 +242,13 @@ function likec4Fences({ root, projects } = {}) {
169
242
  try {
170
243
  layout = await layoutSource(source);
171
244
  } catch (error) {
172
- throw new Error(`${where} is not valid LikeC4 — ${error instanceof Error ? error.message : String(error)}`);
245
+ fail(`${where} is not valid LikeC4 — ${error instanceof Error ? error.message : String(error)}`);
246
+ return;
173
247
  }
174
248
  const viewId = options.view ?? defaultView(source, layout.viewIds);
175
249
  if (!viewId || !layout.viewIds.includes(viewId)) {
176
- throw new Error(`${where} has no view "${viewId ?? "(none)"}". Declared: ${layout.viewIds.join(", ") || "(none)"}.`);
250
+ fail(`${where} has no view "${viewId ?? "(none)"}". Declared: ${layout.viewIds.join(", ") || "(none)"}.`);
251
+ return;
177
252
  }
178
253
  if (markdown) {
179
254
  ctx.replaceNode(node, {
@@ -198,20 +273,18 @@ function likec4Fences({ root, projects } = {}) {
198
273
  };
199
274
  };
200
275
  }
201
- function checkProject(project, projects, where) {
276
+ function projectError(project, projects, where) {
202
277
  if (projects === undefined)
203
278
  return;
204
279
  const requested = project?.trim();
205
280
  const available = projects.join(", ");
206
281
  if (!requested) {
207
- if (projects.length > 1) {
208
- throw new Error(`${where}: ${STRINGS["likec4.error.projectRequired"].replace("{available}", available)}`);
209
- }
210
- return;
282
+ return projects.length > 1 ? `${where}: ${STRINGS["likec4.error.projectRequired"].replace("{available}", available)}` : undefined;
211
283
  }
212
284
  if (!projects.includes(requested)) {
213
- throw new Error(`${where}: ${STRINGS["likec4.error.unknownProject"].replace("{projectId}", requested).replace("{available}", available || "(none)")}`);
285
+ return `${where}: ${STRINGS["likec4.error.unknownProject"].replace("{projectId}", requested).replace("{available}", available || "(none)")}`;
214
286
  }
287
+ return;
215
288
  }
216
289
  function defaultView(source, viewIds) {
217
290
  for (const match of source.matchAll(/(?<![\w-])view\s+([\w-]+)/g)) {
@@ -702,7 +775,10 @@ var OPTIMIZED_DEPENDENCIES = ["react/compiler-runtime"];
702
775
  var MD_FENCES_SCRIPT = [
703
776
  `const mountLikeC4Fences = () => {`,
704
777
  ` if (!document.querySelector('[data-astroc4-fence]:not([data-astroc4-mounted])')) return;`,
705
- ` import('${PACKAGE_NAME2}/components/md-fences').then((module) => module.mountFences());`,
778
+ ` const preamble = window.$RefreshReg$ ? Promise.resolve() : import('@vitejs/plugin-react/preamble');`,
779
+ ` preamble`,
780
+ ` .then(() => import('${PACKAGE_NAME2}/components/md-fences'))`,
781
+ ` .then((module) => module.mountFences());`,
706
782
  `};`,
707
783
  `if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mountLikeC4Fences);`,
708
784
  `else mountLikeC4Fences();`,
@@ -725,7 +801,8 @@ function resolvesFrom(specifier, root) {
725
801
  function likec4Integration({
726
802
  vitePlugin = {},
727
803
  projects,
728
- fences = true
804
+ fences = true,
805
+ validation
729
806
  } = {}) {
730
807
  let icons;
731
808
  return {
@@ -744,7 +821,7 @@ function likec4Integration({
744
821
  }
745
822
  });
746
823
  if (fences) {
747
- await registerFences(config, updateConfig, logger, resolved);
824
+ await registerFences(config, updateConfig, logger, resolved, validation);
748
825
  injectScript("page", MD_FENCES_SCRIPT);
749
826
  }
750
827
  await ensureReactRenderer(config, updateConfig, logger);
@@ -793,12 +870,15 @@ async function ensureReactRenderer(config, updateConfig, logger) {
793
870
  logger.warn("@astrojs/react is not installed, so <LikeC4View> cannot render. Install it (`astro add react`).");
794
871
  }
795
872
  }
796
- async function registerFences(config, updateConfig, logger, projects) {
873
+ async function registerFences(config, updateConfig, logger, projects, validation) {
797
874
  const { satteri, isSatteriProcessor } = await import("@astrojs/markdown-satteri");
798
875
  const existing = config.markdown?.processor;
799
876
  const fences = () => likec4Fences({
800
877
  root: fileURLToPath4(config.root),
801
- projects: projects.length > 0 ? projects.map((project) => project.name) : undefined
878
+ contentDir: config.srcDir ? fileURLToPath4(new URL("content/", config.srcDir)) : undefined,
879
+ projects: projects.length > 0 ? projects.map((project) => project.name) : undefined,
880
+ validation,
881
+ onInvalid: (message) => logger.warn(message)
802
882
  });
803
883
  if (existing === undefined) {
804
884
  updateConfig({ markdown: { processor: satteri({ mdastPlugins: [fences()] }) } });
@@ -1645,7 +1725,8 @@ function starlightLikeC4(options = {}) {
1645
1725
  const integration = likec4Integration({
1646
1726
  vitePlugin: options.vitePlugin,
1647
1727
  projects: options.projects,
1648
- fences: options.fences
1728
+ fences: options.fences,
1729
+ validation: options.validation
1649
1730
  });
1650
1731
  return {
1651
1732
  name: PACKAGE_NAME2,
@@ -1731,5 +1812,5 @@ export {
1731
1812
  withLikeC4Language
1732
1813
  };
1733
1814
 
1734
- //# debugId=E908D3218F3E812A64756E2164756E21
1815
+ //# debugId=4A4B67FAE347BDD864756E2164756E21
1735
1816
  //# sourceMappingURL=index.js.map