@samva/markup 0.18.1 → 0.19.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 (54) hide show
  1. package/AGENTS.md +11 -4
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +89 -5
  4. package/dist/assets-BSpgZOSR.js +61 -0
  5. package/dist/assets-k7_v3IOJ.d.ts +39 -0
  6. package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +15 -5
  10. package/dist/compatibility.js +48 -5
  11. package/dist/compile-C1GWLQ5C.js +490 -0
  12. package/dist/compile-CIeuGmP-.d.ts +98 -0
  13. package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
  14. package/dist/compiler.d.ts +4 -89
  15. package/dist/compiler.js +8 -486
  16. package/dist/components-DNBCz20_.js +3267 -0
  17. package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
  18. package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
  19. package/dist/diagnostic-model-DJtqbETd.js +36 -0
  20. package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
  21. package/dist/diagnostics.d.ts +33 -1
  22. package/dist/diagnostics.js +2 -1
  23. package/dist/edit.d.ts +15 -4
  24. package/dist/edit.js +67 -4
  25. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.d.ts +1 -1
  27. package/dist/email.d.ts +6 -2
  28. package/dist/email.js +6 -2
  29. package/dist/index.d.ts +1 -1
  30. package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
  31. package/dist/markdown.d.ts +69 -0
  32. package/dist/markdown.js +95 -0
  33. package/dist/parse-Bex7QFqj.js +921 -0
  34. package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
  35. package/dist/profile-DFk4gs7o.js +56 -0
  36. package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
  37. package/dist/render.d.ts +8 -8
  38. package/dist/render.js +1 -2
  39. package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
  40. package/dist/sms.d.ts +1 -1
  41. package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
  42. package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/generated/contract.json +41 -1
  45. package/generated/reference.md +97 -89
  46. package/package.json +15 -4
  47. package/dist/brand-CIiEqkU7.js +0 -193
  48. package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
  49. package/dist/components-C5850-Qg.js +0 -409
  50. package/dist/dark-scheme-CbCwGDWH.js +0 -12
  51. package/dist/html-DJUK2YJF.js +0 -483
  52. package/dist/render-template-DwPDm3Hq.js +0 -1226
  53. package/dist/selectors-CYWCoxZr.js +0 -157
  54. package/dist/source-locations-C8wCsiAy.d.ts +0 -22
@@ -312,4 +312,15 @@ const downlevelColors = (value) => {
312
312
  return result;
313
313
  };
314
314
  //#endregion
315
- export { parseColor as n, downlevelColors as t };
315
+ //#region src/email/dark-scheme.ts
316
+ /** The media query a theme's dark-mode block and every generated dark-mode rule sit in. */
317
+ const DARK_SCHEME_MEDIA = "@media (prefers-color-scheme: dark)";
318
+ const DARK_SCHEME_QUERY = /^(?:only\s+)?(?:screen\s+and\s+)?\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
319
+ /** Whether a `@media` prelude is exactly the dark-mode query. */
320
+ const isDarkSchemeQuery = (prelude) => DARK_SCHEME_QUERY.test(prelude.trim());
321
+ /** Whether a rule's enclosing conditions are exactly the dark-mode query. */
322
+ const isDarkSchemeCondition = (conditions) => conditions.length === 1 && /^@media\s/i.test(conditions[0]) && isDarkSchemeQuery(conditions[0].replace(/^@media\s+/i, ""));
323
+ /** Whether any enclosing condition is the dark-mode query, as in a size query nested inside it. */
324
+ const withinDarkScheme = (conditions) => conditions.some((condition) => isDarkSchemeCondition([condition]));
325
+ //#endregion
326
+ export { downlevelColors as a, withinDarkScheme as i, isDarkSchemeCondition as n, parseColor as o, isDarkSchemeQuery as r, DARK_SCHEME_MEDIA as t };
@@ -1,41 +1,34 @@
1
- //#region src/diagnostic-model.ts
2
- /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
- const buildLineMap = (source) => {
4
- const lineStarts = [0];
5
- for (let index = 0; index < source.length; index++) {
6
- const code = source.charCodeAt(index);
7
- if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
- }
9
- return lineStarts;
10
- };
11
- /** 1-based line/column for an absolute offset, via binary search. */
12
- const lineColumn = (lineMap, offset) => {
13
- let low = 0;
14
- let high = lineMap.length - 1;
15
- while (low < high) {
16
- const middle = low + high + 1 >> 1;
17
- if (lineMap[middle] <= offset) low = middle;
18
- else high = middle - 1;
19
- }
20
- return {
21
- line: low + 1,
22
- column: offset - lineMap[low] + 1
23
- };
24
- };
25
- /** Build one formatter for a batch of diagnostics against the same source. */
26
- const createDiagnosticFormatter = (source) => {
27
- const lineMap = buildLineMap(source);
28
- return (diagnostic) => {
29
- const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
- return `line ${line}:${column} ${diagnostic.message}`;
31
- };
32
- };
33
- /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
- const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
- //#endregion
36
1
  //#region src/diagnostic-codes.ts
37
2
  const codes = (table) => table;
38
3
  const DIAGNOSTIC_CODES = codes({
4
+ "issue-layout": {
5
+ summary: "A newsletter issue layout does not declare only an email channel.",
6
+ fix: "Use an email-only defineTemplate layout with one IssueContent slot."
7
+ },
8
+ "issue-slot": {
9
+ summary: "An issue layout does not contain exactly one unconditional content slot.",
10
+ fix: "Import IssueContent from @samva/markup/email and place one <IssueContent /> outside loops and conditions."
11
+ },
12
+ "markdown-syntax": {
13
+ summary: "An issue contains syntax outside SML Markdown.",
14
+ fix: "Write Markdown prose and supported SML component tags."
15
+ },
16
+ "markdown-expression": {
17
+ summary: "An issue contains an expression or spread, which is never evaluated.",
18
+ fix: "Replace expressions with literal prose or quoted static component attributes."
19
+ },
20
+ "markdown-html": {
21
+ summary: "An issue contains raw HTML rather than a supported SML component.",
22
+ fix: "Use Markdown syntax or a component from the closed SML component set."
23
+ },
24
+ "markdown-frontmatter": {
25
+ summary: "Issue frontmatter is missing or does not match the typed contract.",
26
+ fix: "Provide title, slug and topic strings; use only the documented preheader, seo and gate fields."
27
+ },
28
+ "markdown-prop": {
29
+ summary: "A Markdown component has an unknown, missing or invalid literal prop.",
30
+ fix: "Use the component's documented props with quoted static values or bare boolean flags."
31
+ },
39
32
  "no-template": {
40
33
  summary: "The entry does not default-export a template.",
41
34
  fix: "Default-export defineTemplate({ ... }) imported from @samva/markup."
@@ -392,6 +385,10 @@ const DIAGNOSTIC_CODES = codes({
392
385
  summary: "The project imports two different brands.",
393
386
  fix: "Import one brand specifier everywhere."
394
387
  },
388
+ "email-component-degradation": {
389
+ summary: "A component loses content or behavior when lowered to email.",
390
+ fix: "Compare the email and web previews; provide an email-safe equivalent for content readers need."
391
+ },
395
392
  "caniemail/unknown-coverage": {
396
393
  summary: "A feature has no client-support data.",
397
394
  fix: "Verify it in the target clients."
@@ -470,4 +467,4 @@ const DIAGNOSTIC_CODE_FAMILIES = { "caniemail/": {
470
467
  fix: "Use the alternative the message names, such as an email component instead of display: flex."
471
468
  } };
472
469
  //#endregion
473
- export { createDiagnosticFormatter as a, buildLineMap as i, DIAGNOSTIC_CODE_FAMILIES as n, formatDiagnostic as o, hasUpgradeRecipe as r, lineColumn as s, DIAGNOSTIC_CODES as t };
470
+ export { DIAGNOSTIC_CODE_FAMILIES as n, hasUpgradeRecipe as r, DIAGNOSTIC_CODES as t };
@@ -0,0 +1,36 @@
1
+ //#region src/diagnostic-model.ts
2
+ /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
+ const buildLineMap = (source) => {
4
+ const lineStarts = [0];
5
+ for (let index = 0; index < source.length; index++) {
6
+ const code = source.charCodeAt(index);
7
+ if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
+ }
9
+ return lineStarts;
10
+ };
11
+ /** 1-based line/column for an absolute offset, via binary search. */
12
+ const lineColumn = (lineMap, offset) => {
13
+ let low = 0;
14
+ let high = lineMap.length - 1;
15
+ while (low < high) {
16
+ const middle = low + high + 1 >> 1;
17
+ if (lineMap[middle] <= offset) low = middle;
18
+ else high = middle - 1;
19
+ }
20
+ return {
21
+ line: low + 1,
22
+ column: offset - lineMap[low] + 1
23
+ };
24
+ };
25
+ /** Build one formatter for a batch of diagnostics against the same source. */
26
+ const createDiagnosticFormatter = (source) => {
27
+ const lineMap = buildLineMap(source);
28
+ return (diagnostic) => {
29
+ const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
+ return `line ${line}:${column} ${diagnostic.message}`;
31
+ };
32
+ };
33
+ /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
+ const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
+ //#endregion
36
+ export { lineColumn as i, createDiagnosticFormatter as n, formatDiagnostic as r, buildLineMap as t };
@@ -1,4 +1,24 @@
1
- import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
1
+ //#region src/source-locations.d.ts
2
+ /**
3
+ * The source payload a JSX transform may attach to an element. The transform may omit any field
4
+ * for synthetic calls; only a complete payload can identify a source origin safely.
5
+ */
6
+ interface JsxSource {
7
+ readonly fileName?: string | undefined;
8
+ readonly lineNumber?: number | undefined;
9
+ readonly columnNumber?: number | undefined;
10
+ }
11
+ /** A complete, normalized source origin for a JSX element. */
12
+ interface JsxSourceLocation {
13
+ readonly fileName: string;
14
+ readonly lineNumber: number;
15
+ readonly columnNumber: number;
16
+ }
17
+ /** The JSX prop that carries a {@link JsxSource} to the email JSX runtime. */
18
+ declare const JSX_SOURCE_LOCATION_PROP: "__samva_source_location";
19
+ /** Normalize the optional payload a JSX transform attaches. */
20
+ declare const completeJsxSourceLocation: (source: JsxSource | null | undefined) => JsxSourceLocation | undefined;
21
+ //#endregion
2
22
  //#region src/email/diagnostics.d.ts
3
23
  type EmailDiagnosticSeverity = "error" | "warning" | "info";
4
24
  /** What the emitted document establishes beyond the client feature matrix. */
@@ -39,4 +59,4 @@ declare const formatEmailDiagnostic: (diagnostic: TemplateDiagnostic) => string;
39
59
  /** True when a set of findings must stop the compile or refuse a publication. */
40
60
  declare const hasBlockingDiagnostic: (diagnostics: readonly EmailDiagnostic[]) => boolean;
41
61
  //#endregion
42
- export { formatEmailDiagnostic as a, TemplateDiagnostic as i, EmailDiagnostic as n, hasBlockingDiagnostic as o, EmailDiagnosticSeverity as r, EmailCompatibilityAssessment as t };
62
+ export { formatEmailDiagnostic as a, JsxSource as c, TemplateDiagnostic as i, JsxSourceLocation as l, EmailDiagnostic as n, hasBlockingDiagnostic as o, EmailDiagnosticSeverity as r, JSX_SOURCE_LOCATION_PROP as s, EmailCompatibilityAssessment as t, completeJsxSourceLocation as u };
@@ -1,4 +1,4 @@
1
- import { a as formatEmailDiagnostic, i as TemplateDiagnostic, n as EmailDiagnostic, o as hasBlockingDiagnostic, r as EmailDiagnosticSeverity, t as EmailCompatibilityAssessment } from "./diagnostics-D4vSLQrO.js";
1
+ import { a as formatEmailDiagnostic, i as TemplateDiagnostic, n as EmailDiagnostic, o as hasBlockingDiagnostic, r as EmailDiagnosticSeverity, t as EmailCompatibilityAssessment } from "./diagnostics-C3TSx6SU.js";
2
2
  import { a as createDiagnosticFormatter, i as buildLineMap, n as DiagnosticRepair, o as formatDiagnostic, r as Span, s as lineColumn, t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
3
3
  //#region src/diagnostic-codes.d.ts
4
4
  interface DiagnosticCodeSpec {
@@ -12,6 +12,34 @@ interface DiagnosticCodeSpec {
12
12
  readonly channelOnly?: true;
13
13
  }
14
14
  declare const DIAGNOSTIC_CODES: {
15
+ readonly "issue-layout": {
16
+ readonly summary: "A newsletter issue layout does not declare only an email channel.";
17
+ readonly fix: "Use an email-only defineTemplate layout with one IssueContent slot.";
18
+ };
19
+ readonly "issue-slot": {
20
+ readonly summary: "An issue layout does not contain exactly one unconditional content slot.";
21
+ readonly fix: "Import IssueContent from @samva/markup/email and place one <IssueContent /> outside loops and conditions.";
22
+ };
23
+ readonly "markdown-syntax": {
24
+ readonly summary: "An issue contains syntax outside SML Markdown.";
25
+ readonly fix: "Write Markdown prose and supported SML component tags.";
26
+ };
27
+ readonly "markdown-expression": {
28
+ readonly summary: "An issue contains an expression or spread, which is never evaluated.";
29
+ readonly fix: "Replace expressions with literal prose or quoted static component attributes.";
30
+ };
31
+ readonly "markdown-html": {
32
+ readonly summary: "An issue contains raw HTML rather than a supported SML component.";
33
+ readonly fix: "Use Markdown syntax or a component from the closed SML component set.";
34
+ };
35
+ readonly "markdown-frontmatter": {
36
+ readonly summary: "Issue frontmatter is missing or does not match the typed contract.";
37
+ readonly fix: "Provide title, slug and topic strings; use only the documented preheader, seo and gate fields.";
38
+ };
39
+ readonly "markdown-prop": {
40
+ readonly summary: "A Markdown component has an unknown, missing or invalid literal prop.";
41
+ readonly fix: "Use the component's documented props with quoted static values or bare boolean flags.";
42
+ };
15
43
  readonly "no-template": {
16
44
  readonly summary: "The entry does not default-export a template.";
17
45
  readonly fix: "Default-export defineTemplate({ ... }) imported from @samva/markup.";
@@ -356,6 +384,10 @@ declare const DIAGNOSTIC_CODES: {
356
384
  readonly summary: "The project imports two different brands.";
357
385
  readonly fix: "Import one brand specifier everywhere.";
358
386
  };
387
+ readonly "email-component-degradation": {
388
+ readonly summary: "A component loses content or behavior when lowered to email.";
389
+ readonly fix: "Compare the email and web previews; provide an email-safe equivalent for content readers need.";
390
+ };
359
391
  readonly "caniemail/unknown-coverage": {
360
392
  readonly summary: "A feature has no client-support data.";
361
393
  readonly fix: "Verify it in the target clients.";
@@ -1,3 +1,4 @@
1
1
  import { n as formatEmailDiagnostic, r as hasBlockingDiagnostic } from "./diagnostics-C0Tr8-eQ.js";
2
- import { a as createDiagnosticFormatter, i as buildLineMap, n as DIAGNOSTIC_CODE_FAMILIES, o as formatDiagnostic, r as hasUpgradeRecipe, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-DZKb0Xcg.js";
2
+ import { i as lineColumn, n as createDiagnosticFormatter, r as formatDiagnostic, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
3
+ import { n as DIAGNOSTIC_CODE_FAMILIES, r as hasUpgradeRecipe, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BCwfUgnF.js";
3
4
  export { DIAGNOSTIC_CODES, DIAGNOSTIC_CODE_FAMILIES, buildLineMap, createDiagnosticFormatter, formatDiagnostic, formatEmailDiagnostic, hasBlockingDiagnostic, hasUpgradeRecipe, lineColumn };
package/dist/edit.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as completeJsxSourceLocation, n as JsxSource, r as JsxSourceLocation, t as JSX_SOURCE_LOCATION_PROP } from "./source-locations-C8wCsiAy.js";
2
- import { a as emailAuthoringTarget, c as emailSelectionIndex, d as previewEmail, i as INSTANCE_PATH_ATTRIBUTE, l as emailSelectionSiblings, n as EmailPreview, o as emailDefinitionInstances, r as EmittedPosition, s as emailSelectionAncestry, t as EmailElementSelection, u as emailSelections } from "./preview-rNoJBEIY.js";
1
+ import { c as JsxSource, l as JsxSourceLocation, s as JSX_SOURCE_LOCATION_PROP, u as completeJsxSourceLocation } from "./diagnostics-C3TSx6SU.js";
2
+ import { a as emailAuthoringTarget, c as emailSelectionIndex, d as previewEmail, i as INSTANCE_PATH_ATTRIBUTE, l as emailSelectionSiblings, n as EmailPreview, o as emailDefinitionInstances, r as EmittedPosition, s as emailSelectionAncestry, t as EmailElementSelection, u as emailSelections } from "./preview-DRLWgmfg.js";
3
3
  import { n as WhatsAppCategory, r as WhatsAppHeaderType, t as SmsCategory } from "./channel-specs-C2feJwuP.js";
4
4
  //#region src/source-edit.d.ts
5
5
  /** One exact source replacement; before text guards against concurrent changes. */
@@ -23,7 +23,7 @@ interface SourceProperty {
23
23
  readonly literalType: "string" | "number" | "boolean";
24
24
  readonly start: number;
25
25
  readonly end: number;
26
- readonly syntax: "attribute" | "child" | "value";
26
+ readonly syntax: "attribute" | "child" | "value" | "markdown" | "markdown-attribute" | "markdown-boolean";
27
27
  }
28
28
  interface SourceElement {
29
29
  readonly start: number;
@@ -45,6 +45,13 @@ interface SourceRange {
45
45
  }
46
46
  /** Resolve a renderer origin against the unchanged authored entry, never against HTML offsets. */
47
47
  declare const inspectSourceElement: (source: string, file: string, origin: JsxSourceLocation) => SourceElement | null;
48
+ /**
49
+ * Resolve a Markdown render origin to authored prose or component attributes. A tag distinguishes
50
+ * nodes sharing an origin; without one, prefer an editable authored element over its wrapper.
51
+ */
52
+ declare const inspectMarkdownNode: (source: string, file: string, origin: JsxSourceLocation, options?: {
53
+ readonly tag?: string | undefined;
54
+ }) => SourceElement | null;
48
55
  /** Literal controls cannot overwrite a binding or computed expression. */
49
56
  declare const literalReplacement: (source: string, field: SourceProperty, value: string) => SourceReplacement | null;
50
57
  /** Explicit expression changes are proposals: callers show before/after and require Apply. */
@@ -63,6 +70,10 @@ interface ProfileGuard {
63
70
  readonly files?: Readonly<Record<string, string>> | undefined;
64
71
  }
65
72
  interface ApplyOptions {
73
+ /** Validate edits as an issue, including its closed grammar and frontmatter. */
74
+ readonly markdown?: {
75
+ readonly file: string;
76
+ } | undefined;
66
77
  readonly profile?: ProfileGuard | undefined;
67
78
  }
68
79
  /** Apply exact, nonoverlapping edits and refuse syntax damage without changing the original. */
@@ -230,4 +241,4 @@ declare const readWhatsAppChannel: (source: string) => WhatsAppChannelForm | und
230
241
  /** The source replacements that apply one form change to the WhatsApp body. */
231
242
  declare const editWhatsAppChannel: (source: string, edit: WhatsAppEdit) => ChannelEditResult;
232
243
  //#endregion
233
- export { ApplyOptions, AttributeState, ChannelEditResult, ChannelSegment, ClassNameOperation, type EmailElementSelection, type EmailPreview, type EmittedPosition, INSTANCE_PATH_ATTRIBUTE, JSX_SOURCE_LOCATION_PROP, JsxSource, JsxSourceLocation, LockedSegment, ProfileGuard, type SmsCategory, SmsChannelForm, SmsEdit, SourceEditResult, SourceElement, SourceFixture, SourceProperty, SourceProposal, SourceRange, SourceReplacement, TextSegment, WhatsAppButtonKind, WhatsAppButtonRow, type WhatsAppCategory, WhatsAppChannelForm, WhatsAppEdit, type WhatsAppHeaderType, WhatsAppPart, applySourceReplacements, classNameEdit, completeJsxSourceLocation, editSmsChannel, editWhatsAppChannel, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, previewEmail, readSmsChannel, readWhatsAppChannel, structuralReplacements };
244
+ export { ApplyOptions, AttributeState, ChannelEditResult, ChannelSegment, ClassNameOperation, type EmailElementSelection, type EmailPreview, type EmittedPosition, INSTANCE_PATH_ATTRIBUTE, JSX_SOURCE_LOCATION_PROP, JsxSource, JsxSourceLocation, LockedSegment, ProfileGuard, type SmsCategory, SmsChannelForm, SmsEdit, SourceEditResult, SourceElement, SourceFixture, SourceProperty, SourceProposal, SourceRange, SourceReplacement, TextSegment, WhatsAppButtonKind, WhatsAppButtonRow, type WhatsAppCategory, WhatsAppChannelForm, WhatsAppEdit, type WhatsAppHeaderType, WhatsAppPart, applySourceReplacements, classNameEdit, completeJsxSourceLocation, editSmsChannel, editWhatsAppChannel, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections, expressionReplacement, inspectMarkdownNode, inspectSourceElement, inspectSourceFixtures, literalReplacement, previewEmail, readSmsChannel, readWhatsAppChannel, structuralReplacements };
package/dist/edit.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
- import { c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, h as previewEmail, l as emailAuthoringTarget, m as emailSelections, p as emailSelectionSiblings, u as emailDefinitionInstances } from "./html-DJUK2YJF.js";
2
+ import { C as emailSelectionAncestry, D as previewEmail, E as emailSelections, S as emailDefinitionInstances, T as emailSelectionSiblings, b as INSTANCE_PATH_ATTRIBUTE, w as emailSelectionIndex, x as emailAuthoringTarget } from "./components-DNBCz20_.js";
3
3
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-qL19u3Fy.js";
4
- import { i as buildLineMap } from "./diagnostic-codes-DZKb0Xcg.js";
5
- import { c as SMS_CATEGORIES, l as WHATSAPP_CATEGORIES, n as introducedErrors, t as checkTemplate, u as WHATSAPP_HEADER_TYPES } from "./profile-JTvgCD6X.js";
4
+ import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
5
+ import { c as WHATSAPP_HEADER_TYPES, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES } from "./project-B6D64SZS.js";
6
+ import { n as introducedErrors, t as checkTemplate } from "./profile-DFk4gs7o.js";
7
+ import { t as parseIssue } from "./parse-Bex7QFqj.js";
6
8
  import { parse } from "@babel/parser";
7
9
  //#region src/source-edit.ts
8
10
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
@@ -118,12 +120,63 @@ const inspectSourceElement = (source, file, origin) => {
118
120
  });
119
121
  return found;
120
122
  };
123
+ /**
124
+ * Resolve a Markdown render origin to authored prose or component attributes. A tag distinguishes
125
+ * nodes sharing an origin; without one, prefer an editable authored element over its wrapper.
126
+ */
127
+ const inspectMarkdownNode = (source, file, origin, options = {}) => {
128
+ if (file !== origin.fileName) return null;
129
+ const issue = parseIssue(source, file);
130
+ if (issue.diagnostics.some((item) => item.severity === "error")) return null;
131
+ const candidates = issue.sourceNodes.filter((item) => item.src[0] === origin.lineNumber && item.src[1] === origin.columnNumber && item.kind !== "text");
132
+ const found = options.tag === void 0 ? candidates.find((item) => item.properties.length > 0) ?? candidates[0] : candidates.find((item) => item.tag === options.tag);
133
+ if (found === void 0) return null;
134
+ return {
135
+ start: found.start,
136
+ end: found.end,
137
+ contentStart: found.contentStart,
138
+ contentEnd: found.contentEnd,
139
+ tag: found.tag,
140
+ attributeInsertion: found.contentStart ?? found.end,
141
+ hasSpread: false,
142
+ structural: false,
143
+ previous: null,
144
+ next: null,
145
+ properties: found.properties.map((field) => ({
146
+ ...field,
147
+ value: String(field.value),
148
+ kind: "literal",
149
+ syntax: field.syntax === "quoted" ? "markdown-attribute" : field.syntax === "boolean" ? "markdown-boolean" : "markdown"
150
+ }))
151
+ };
152
+ };
121
153
  const PLAIN_JSX_TEXT = /^[^\s{}<>&](?:[^{}<>&\r\n\u2028\u2029]*[^\s{}<>&])?$/;
154
+ /** Keep prose readable while protecting inline syntax and the surrounding block's delimiters. */
155
+ const escapeMarkdownProse = (source, field, value) => {
156
+ let escaped = value.replace(/[\\`*_[\]<{]/g, "\\$&").replace(/&(?=(?:[a-z][a-z\d]{1,31}|#\d{1,7}|#x[\da-f]{1,6});)/gi, "\\&").replace(/[\r\n\t]|^ +| +$/g, (whitespace) => [...whitespace].map((character) => `&#${character.charCodeAt(0)};`).join(""));
157
+ const block = parseIssue(source, "source.md").sourceNodes.find((authored) => authored.kind === "block" && authored.properties.some((candidate) => candidate.name === "children" && candidate.start === field.start && candidate.end === field.end));
158
+ const prefix = block === void 0 ? void 0 : source.slice(block.start, field.start);
159
+ if (prefix !== void 0 && /^ {0,3}$/.test(prefix)) escaped = escaped.replace(/^(?:#{1,6}(?=[ \t]|$)|>|[-+](?=[ \t]|$)|(?:-+|=+)(?=[ \t]*$)|~{3,})/, "\\$&").replace(/^(\d{1,9})([.)])(?=[ \t]|$)/, "$1\\$2");
160
+ const lineEnd = source.slice(field.end).search(/[\r\n]/);
161
+ const suffix = source.slice(field.end, lineEnd < 0 ? source.length : field.end + lineEnd);
162
+ if (prefix !== void 0 && /^ {0,3}#{1,6}[ \t]+$/.test(prefix) && /^[ \t]*$/.test(suffix)) escaped = escaped.replace(/(^|[ \t])(#+)$/, "$1\\$2");
163
+ return escaped;
164
+ };
122
165
  /** Literal controls cannot overwrite a binding or computed expression. */
123
166
  const literalReplacement = (source, field, value) => {
124
167
  if (field.kind !== "literal") return null;
125
168
  if (field.literalType === "number" && (value.trim() === "" || !Number.isFinite(Number(value)))) return null;
126
169
  if (field.literalType === "boolean" && value !== "true" && value !== "false") return null;
170
+ if (field.syntax === "markdown" || field.syntax === "markdown-attribute" || field.syntax === "markdown-boolean") {
171
+ const encoded = value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll("{", "&#123;").replaceAll("}", "&#125;").replace(/[\r\n\t]/g, (character) => `&#${character.charCodeAt(0)};`);
172
+ const markdown = field.name === "children" ? escapeMarkdownProse(source, field, value) : value.replace(/[!-/:-@[-`{-~]/g, "\\$&").replace(field.name === "href" || field.name === "src" ? /\s/g : /[\r\n\t]|^ +| +$/g, (whitespace) => [...whitespace].map((character) => `&#${character.charCodeAt(0)};`).join(""));
173
+ return {
174
+ start: field.start,
175
+ end: field.end,
176
+ before: source.slice(field.start, field.end),
177
+ after: field.syntax === "markdown" ? markdown : field.syntax === "markdown-boolean" ? `${field.name}="${encoded}"` : `"${encoded}"`
178
+ };
179
+ }
127
180
  const literal = field.literalType === "string" ? JSON.stringify(value) : field.literalType === "number" ? String(Number(value)) : value;
128
181
  const before = source.slice(field.start, field.end);
129
182
  const plainText = field.syntax === "child" && !before.startsWith("{") && PLAIN_JSX_TEXT.test(value);
@@ -166,6 +219,16 @@ const applySourceReplacements = (source, edits, options = {}) => {
166
219
  result = result.slice(0, edit.start) + edit.after + result.slice(edit.end);
167
220
  boundary = edit.start;
168
221
  }
222
+ if (options.markdown !== void 0) {
223
+ const finding = parseIssue(result, options.markdown.file).diagnostics.find((item) => item.severity === "error");
224
+ return finding === void 0 ? {
225
+ ok: true,
226
+ source: result
227
+ } : {
228
+ ok: false,
229
+ reason: `This change is not valid SML Markdown: ${finding.message}`
230
+ };
231
+ }
169
232
  if (parsed(result) === null) return {
170
233
  ok: false,
171
234
  reason: "This change is not valid TSX. Review it in Source."
@@ -940,4 +1003,4 @@ const editWhatsAppChannel = (source, edit) => {
940
1003
  }
941
1004
  };
942
1005
  //#endregion
943
- export { INSTANCE_PATH_ATTRIBUTE, JSX_SOURCE_LOCATION_PROP, applySourceReplacements, classNameEdit, completeJsxSourceLocation, editSmsChannel, editWhatsAppChannel, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, previewEmail, readSmsChannel, readWhatsAppChannel, structuralReplacements };
1006
+ export { INSTANCE_PATH_ATTRIBUTE, JSX_SOURCE_LOCATION_PROP, applySourceReplacements, classNameEdit, completeJsxSourceLocation, editSmsChannel, editWhatsAppChannel, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections, expressionReplacement, inspectMarkdownNode, inspectSourceElement, inspectSourceFixtures, literalReplacement, previewEmail, readSmsChannel, readWhatsAppChannel, structuralReplacements };
@@ -1,5 +1,5 @@
1
- import { n as JsxSource } from "../source-locations-C8wCsiAy.js";
2
- import { a as Fragment, i as EmailNode, n as EmailComponent, o as JSX } from "../jsx-runtime-DL8NVF1f.js";
1
+ import { c as JsxSource } from "../diagnostics-C3TSx6SU.js";
2
+ import { a as Fragment, i as EmailNode, n as EmailComponent, o as JSX } from "../jsx-runtime-HHfrVZEf.js";
3
3
  //#region src/email/jsx-dev-runtime.d.ts
4
4
  /** Development JSX retains compiler-provided origins directly on the semantic tree. */
5
5
  declare const jsxDEV: (type: string | EmailComponent<Record<string, unknown>> | typeof Fragment, props: Record<string, unknown> | null, _key?: unknown, _staticChildren?: boolean, source?: JsxSource) => EmailNode;
@@ -1,2 +1,2 @@
1
- import { a as Fragment, c as currentEmailOrigins, d as normalizeChildren, f as rawMarkup, i as EmailNode, l as jsx, n as EmailComponent, o as JSX, p as withEmailDiagnostics, r as EmailFinding, s as collectEmailDiagnostics, t as EmailChild, u as jsxs } from "../jsx-runtime-DL8NVF1f.js";
1
+ import { a as Fragment, c as currentEmailOrigins, d as normalizeChildren, f as rawMarkup, i as EmailNode, l as jsx, n as EmailComponent, o as JSX, p as withEmailDiagnostics, r as EmailFinding, s as collectEmailDiagnostics, t as EmailChild, u as jsxs } from "../jsx-runtime-HHfrVZEf.js";
2
2
  export { EmailChild, EmailComponent, EmailFinding, EmailNode, Fragment, JSX, collectEmailDiagnostics, currentEmailOrigins, jsx, jsxs, normalizeChildren, rawMarkup, withEmailDiagnostics };
package/dist/email.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as EmailNode, t as EmailChild, x as EmailStyle } from "./jsx-runtime-DL8NVF1f.js";
1
+ import { i as EmailNode, t as EmailChild, x as EmailStyle } from "./jsx-runtime-HHfrVZEf.js";
2
2
  //#region src/email/components.d.ts
3
3
  /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
4
4
  declare const PREHEADER_ATTRIBUTE = "data-samva-preheader";
@@ -175,4 +175,8 @@ interface LinkProps {
175
175
  */
176
176
  declare const Link: ({ className, href, target, rel, style, children }: LinkProps) => EmailNode;
177
177
  //#endregion
178
- export { Button, ButtonProps, Column, ColumnProps, Columns, ColumnsProps, Divider, DividerProps, Email, EmailProps, Image, ImageProps, Link, LinkProps, PREHEADER_ATTRIBUTE, Section, SectionProps, Spacer, SpacerProps, equalColumnWidth, preheaderNode };
178
+ //#region src/issue-content.d.ts
179
+ /** Static layout directive replaced by parsed issue content during compilation. Takes no props. */
180
+ declare const IssueContent: () => EmailNode | null;
181
+ //#endregion
182
+ export { Button, ButtonProps, Column, ColumnProps, Columns, ColumnsProps, Divider, DividerProps, Email, EmailProps, Image, ImageProps, IssueContent, Link, LinkProps, PREHEADER_ATTRIBUTE, Section, SectionProps, Spacer, SpacerProps, equalColumnWidth, preheaderNode };
package/dist/email.js CHANGED
@@ -1,2 +1,6 @@
1
- import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-C5850-Qg.js";
2
- export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };
1
+ import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-DNBCz20_.js";
2
+ //#region src/issue-content.ts
3
+ /** Static layout directive replaced by parsed issue content during compilation. Takes no props. */
4
+ const IssueContent = () => null;
5
+ //#endregion
6
+ export { Button, Column, Columns, Divider, Email, Image, IssueContent, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as TemplateInput, n as SmlTemplate, o as defineTemplate, r as TEMPLATE_ID_PATTERN, t as EmailChannel } from "./template-DmNcoH4Q.js";
1
+ import { a as TemplateInput, n as SmlTemplate, o as defineTemplate, r as TEMPLATE_ID_PATTERN, t as EmailChannel } from "./template-4Dse7_OC.js";
2
2
  export { type EmailChannel, type SmlTemplate, TEMPLATE_ID_PATTERN, type TemplateInput, defineTemplate };
@@ -1,5 +1,4 @@
1
- import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
2
- import { n as EmailDiagnostic } from "./diagnostics-D4vSLQrO.js";
1
+ import { l as JsxSourceLocation, n as EmailDiagnostic } from "./diagnostics-C3TSx6SU.js";
3
2
  //#region src/email/elements.d.ts
4
3
  /** How one attribute is spelled and what values it accepts. */
5
4
  interface EmailAttributeSpec {
@@ -0,0 +1,69 @@
1
+ import { n as EmailDiagnostic } from "./diagnostics-C3TSx6SU.js";
2
+ import { g as CompiledSource, u as CompiledNode } from "./compiled-template-CPDHvFlt.js";
3
+ import { r as CompileResult, t as CompileTemplateOptions } from "./compile-CIeuGmP-.js";
4
+ //#region src/markdown/types.d.ts
5
+ interface IssueFrontmatter {
6
+ readonly title: string;
7
+ readonly slug: string;
8
+ readonly topic: string;
9
+ readonly preheader?: string;
10
+ readonly seo?: {
11
+ readonly title?: string;
12
+ readonly description?: string;
13
+ readonly image?: string;
14
+ };
15
+ /** Number of leading top-level blocks visible before the subscription gate. */
16
+ readonly gate?: {
17
+ readonly leadIn: number;
18
+ };
19
+ }
20
+ /** Value offsets are UTF-16, end-exclusive; quoted spans include both literal quotes. */
21
+ interface MarkdownSourceProperty {
22
+ readonly name: string;
23
+ readonly value: string | number | boolean;
24
+ readonly literalType: "string" | "number" | "boolean";
25
+ readonly start: number;
26
+ readonly end: number;
27
+ readonly syntax: "quoted" | "boolean" | "markdown";
28
+ }
29
+ /** Authored ranges, before decoding entities or removing Markdown delimiters. */
30
+ interface MarkdownSourceNode {
31
+ readonly kind: "component" | "block" | "inline" | "text";
32
+ readonly start: number;
33
+ readonly end: number;
34
+ readonly contentStart: number;
35
+ readonly contentEnd: number;
36
+ readonly tag: string;
37
+ readonly src: CompiledSource;
38
+ readonly properties: readonly MarkdownSourceProperty[];
39
+ }
40
+ interface ParseIssueResult {
41
+ /** Empty whenever any diagnostic is an error. Brand components await host resolution. */
42
+ readonly nodes: readonly CompiledNode[];
43
+ readonly frontmatter: IssueFrontmatter | undefined;
44
+ readonly diagnostics: readonly EmailDiagnostic[];
45
+ readonly sources: readonly string[];
46
+ readonly sourceNodes: readonly MarkdownSourceNode[];
47
+ }
48
+ //#endregion
49
+ //#region src/markdown/parse.d.ts
50
+ /** Parse a static issue without importing, evaluating or passing through authored HTML. */
51
+ declare const parseIssue: (source: string, file: string) => ParseIssueResult;
52
+ //#endregion
53
+ //#region src/issue.d.ts
54
+ /** An issue's Markdown path and the ordinary TSX template used as its layout. */
55
+ interface CompileIssueOptions extends Omit<CompileTemplateOptions, "entry" | "issueContent"> {
56
+ readonly entry: string;
57
+ readonly layout?: string | undefined;
58
+ }
59
+ interface IssueCompileResult extends CompileResult {
60
+ readonly frontmatter: IssueFrontmatter | undefined;
61
+ }
62
+ /**
63
+ * Compile an issue inside layout.tsx (or the selected layout), without evaluating either file.
64
+ * The slug identifies the issue, title is its email subject, and preheader is its inbox preview.
65
+ * The layout supplies the input schema, fixtures, brand, theme and one IssueContent slot.
66
+ */
67
+ declare const compileIssue: (options: CompileIssueOptions) => Promise<IssueCompileResult>;
68
+ //#endregion
69
+ export { CompileIssueOptions, IssueCompileResult, type IssueFrontmatter, type MarkdownSourceNode, type MarkdownSourceProperty, type ParseIssueResult, compileIssue, parseIssue };
@@ -0,0 +1,95 @@
1
+ import { t as compileTemplate } from "./compile-C1GWLQ5C.js";
2
+ import { t as parseIssue } from "./parse-Bex7QFqj.js";
3
+ //#region src/issue.ts
4
+ /**
5
+ * Compile an issue inside layout.tsx (or the selected layout), without evaluating either file.
6
+ * The slug identifies the issue, title is its email subject, and preheader is its inbox preview.
7
+ * The layout supplies the input schema, fixtures, brand, theme and one IssueContent slot.
8
+ */
9
+ const compileIssue = async (options) => {
10
+ const source = options.files[options.entry];
11
+ if (typeof source !== "string") return {
12
+ template: void 0,
13
+ frontmatter: void 0,
14
+ fixtures: {},
15
+ assets: [],
16
+ diagnostics: [{
17
+ code: "markdown-syntax",
18
+ severity: "error",
19
+ message: `Issue ${options.entry} is missing or is not text.`,
20
+ origins: [{
21
+ fileName: options.entry,
22
+ lineNumber: 1,
23
+ columnNumber: 1
24
+ }]
25
+ }]
26
+ };
27
+ const issue = parseIssue(source, options.entry);
28
+ if (issue.frontmatter === void 0 || issue.diagnostics.some((item) => item.severity === "error")) return {
29
+ template: void 0,
30
+ frontmatter: issue.frontmatter,
31
+ fixtures: {},
32
+ assets: [],
33
+ diagnostics: issue.diagnostics
34
+ };
35
+ const layout = options.layout ?? "layout.tsx";
36
+ if (typeof options.files[layout] !== "string") return {
37
+ template: void 0,
38
+ frontmatter: issue.frontmatter,
39
+ fixtures: {},
40
+ assets: [],
41
+ diagnostics: [{
42
+ code: "issue-layout",
43
+ severity: "error",
44
+ message: `Issue layout ${layout} is missing or is not text.`,
45
+ origins: [{
46
+ fileName: layout,
47
+ lineNumber: 1,
48
+ columnNumber: 1
49
+ }]
50
+ }]
51
+ };
52
+ const compiled = await compileTemplate({
53
+ ...options,
54
+ entry: layout,
55
+ issueContent: {
56
+ file: options.entry,
57
+ nodes: issue.nodes
58
+ }
59
+ });
60
+ const diagnostics = [...issue.diagnostics, ...compiled.diagnostics];
61
+ const template = compiled.template;
62
+ if (template?.email === void 0 || template.sms !== void 0 || template.whatsapp !== void 0) return {
63
+ ...compiled,
64
+ template: void 0,
65
+ frontmatter: issue.frontmatter,
66
+ diagnostics: template === void 0 ? diagnostics : [...diagnostics, {
67
+ code: "issue-layout",
68
+ severity: "error",
69
+ message: "An issue layout must declare only an email channel.",
70
+ origins: [{
71
+ fileName: layout,
72
+ lineNumber: 1,
73
+ columnNumber: 1
74
+ }]
75
+ }]
76
+ };
77
+ const { preheader: _preheader, ...email } = template.email;
78
+ const result = {
79
+ ...template,
80
+ template: issue.frontmatter.slug,
81
+ email: {
82
+ ...email,
83
+ subject: issue.frontmatter.title,
84
+ ...issue.frontmatter.preheader === void 0 ? {} : { preheader: issue.frontmatter.preheader }
85
+ }
86
+ };
87
+ return {
88
+ ...compiled,
89
+ template: result,
90
+ frontmatter: issue.frontmatter,
91
+ diagnostics
92
+ };
93
+ };
94
+ //#endregion
95
+ export { compileIssue, parseIssue };