react-os-shell 4.7.1 → 4.8.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.
@@ -11,6 +11,16 @@
11
11
  * therefore adds a few hundred bytes of first-party code to a bundle and nothing
12
12
  * else — see the subpath's own tsup entry.
13
13
  *
14
+ * It is also the ONE subpath whose `exports` entry carries a `default`
15
+ * condition alongside `import`, and that is deliberate rather than untidy. The
16
+ * root and `./apps` are React component entries; a bundler resolves them and
17
+ * nothing else has any business to. This module is consumed by build scripts
18
+ * too — the storefront's media-manifest generator reaches it through its Puck
19
+ * config — and those run under `tsx`, which resolves through Node's CJS
20
+ * loader. With only `types` and `import` declared, that resolution fails with
21
+ * ERR_PACKAGE_PATH_NOT_EXPORTED, which reads like a missing file rather than a
22
+ * missing condition. A module this portable should answer to any resolver.
23
+ *
14
24
  * WHAT IS SHARED IS THE RULE, NOT THE RENDERING. Each product renders the token
15
25
  * list its own way, because it must: the web uses CSS classes bound to theme
16
26
  * tokens, email must inline every style (mail clients drop classes). Two
@@ -45,6 +55,22 @@
45
55
  * (an intraword underscore is literal) and it is checked with plain character
46
56
  * tests rather than a lookbehind, because a lookbehind is a parse error on
47
57
  * Safari below 16.4 and this code ships to a public storefront.
58
+ *
59
+ * ## One KNOWN divergence from the standard named above
60
+ *
61
+ * `__phrase__` IS ITALIC HERE. CommonMark reads a doubled underscore as STRONG;
62
+ * this grammar has a single `_` rule, so the outer pair matches, the inner
63
+ * underscores are ordinary characters, and `__all__` comes back as `_all_`.
64
+ *
65
+ * Left alone deliberately, and recorded rather than fixed. A census of stored
66
+ * copy found 14 rows in this shape and every one of them is the literal link
67
+ * key `__all__`, which no renderer is handed — so nothing published is being
68
+ * mis-drawn today. Adding a `__` rule to buy CommonMark parity would change how
69
+ * a stored `_` pair parses for the benefit of no existing string, and this
70
+ * module's promise is that the grammar grows while nothing published moves.
71
+ * Bold is `**phrase**` in every product; that is the form the toolbar writes
72
+ * and the form to reach for. Pinned by a test, so it stays a decision rather
73
+ * than becoming a discovery.
48
74
  */
49
75
  /** A style a toolbar button can apply. The first five are inline marks; the
50
76
  * last four are the composer-only affordances (a link and the three line
@@ -82,18 +108,6 @@ interface MarkupTool {
82
108
  }
83
109
  /** Every button, in the order a writer reaches for them. */
84
110
  declare const MARKUP_TOOLS: readonly MarkupTool[];
85
- /**
86
- * The four buttons a COPY FIELD offers — a storefront section's copy box and a
87
- * campaign block's copy box show exactly these, so a merchant meets one toolbar
88
- * wherever they type. Listed here rather than per product for the obvious
89
- * reason: two lists would drift.
90
- *
91
- * The other five are absent on purpose. A storefront section and an email block
92
- * render inline runs only — no lists, no block quotes, no anchor. Offering a
93
- * button whose output the renderer prints as literal text is worse than not
94
- * offering it, so a field advertises exactly what its renderer can draw.
95
- */
96
- declare const COPY_FIELD_TOOLS: readonly MarkStyle[];
97
111
  /** The tools a host shows, resolved to descriptors in {@link MARKUP_TOOLS}
98
112
  * order. Keeps a host from hand-rebuilding a descriptor (and drifting on a
99
113
  * tooltip) just to pick a subset. */
@@ -112,9 +126,21 @@ interface InlineRule {
112
126
  /** CommonMark's `_` rule: neither delimiter may touch a word character, so
113
127
  * `snake_case` and `{{first_name}}` stay literal. */
114
128
  readonly wordBoundary?: boolean;
115
- /** A close followed by a digit does not close, so rank markers ("Ranked #1
116
- * and #2") stay literal instead of pairing up. */
117
- readonly notBeforeDigit?: boolean;
129
+ /**
130
+ * Characters that may not FOLLOW the close. A run whose closing delimiter
131
+ * butts up against one of these does not fire, and both delimiters stay
132
+ * literal.
133
+ *
134
+ * Two legacy rules need it, for the same reason: a delimiter that is also
135
+ * ordinary punctuation will otherwise claim text that was never markup. `#`
136
+ * must not pair across rank markers ("Ranked #1 and #2"), and `[` must not
137
+ * claim the label of a markdown link ("[terms](https://…)"), where the URL
138
+ * would be stranded outside the run and printed to the reader.
139
+ *
140
+ * A character SET rather than a flag per case, so the next collision is one
141
+ * more rule field and no new branch in {@link tokenizeInline}.
142
+ */
143
+ readonly notBefore?: string;
118
144
  }
119
145
  /**
120
146
  * The standard-markdown marks, shared by every product.
@@ -128,6 +154,27 @@ interface InlineRule {
128
154
  * what actually does the work.
129
155
  */
130
156
  declare const STANDARD_MARKUP: readonly InlineRule[];
157
+ /**
158
+ * The buttons a COPY FIELD offers — a storefront section's copy box and a
159
+ * campaign block's copy box show exactly these, so a merchant meets one toolbar
160
+ * wherever they type.
161
+ *
162
+ * DERIVED, not listed. A copy field renders inline runs only — no lists, no
163
+ * block quote, no anchor — so the set it may advertise is precisely the tools
164
+ * whose output this grammar parses back. Writing that as a literal list was the
165
+ * bug's hiding place: the list and the grammar were two hand-maintained facts
166
+ * that agreed by luck, and nothing failed when a tool wrote a delimiter the
167
+ * parser read as something else. Now a style with no rule cannot appear here,
168
+ * whatever anyone types into the array.
169
+ *
170
+ * It resolves to bold, italic, strike and highlight. `code` is the one that
171
+ * surprises: it is an inline mark and it has a fence, but no rule reads a
172
+ * backtick, so a copy field would print `` `word` `` verbatim — and a button
173
+ * whose output the renderer draws as literal text is worse than no button.
174
+ *
175
+ * Declared here, below the rules, because it now depends on them.
176
+ */
177
+ declare const COPY_FIELD_TOOLS: readonly MarkStyle[];
131
178
  /**
132
179
  * The storefront's rule set: standard markdown plus the ONE legacy run it has
133
180
  * published — `*phrase*` in the theme accent. Delete that last entry once
@@ -140,6 +187,15 @@ declare const STOREFRONT_MARKUP: readonly InlineRule[];
140
187
  * runs — `#phrase#` bold, `*phrase*` accent, `[phrase]` italic. The bold guard
141
188
  * is the one the email parser already carried: a `#` followed by a digit does
142
189
  * not close a run, so "Ranked #1 and #2" is literal while "Save #20%#" bolds.
190
+ *
191
+ * The italic guard exists because `[phrase]` collides with the one piece of
192
+ * markdown syntax every writer already knows. Without it, "Read [our
193
+ * terms](https://…) first" italicises the label AND prints the bare URL to the
194
+ * reader — the words come back wearing a mark nobody chose. It is not a
195
+ * toolbar problem: a merchant typing an ordinary link into a copy box hits it,
196
+ * which is why the guard is on the RULE and not on which buttons a field
197
+ * offers. `[phrase]` on its own is untouched, so every published line still
198
+ * reads exactly as it did.
143
199
  */
144
200
  declare const CAMPAIGN_MARKUP: readonly InlineRule[];
145
201
  /** A parsed run. `text` is the inner text with the delimiters removed, so a
@@ -67,12 +67,6 @@ var MARKUP_TOOLS = [
67
67
  { style: "number", label: "1.", title: "Numbered list" },
68
68
  { style: "quote", label: "\u275D", title: "Quote" }
69
69
  ];
70
- var COPY_FIELD_TOOLS = [
71
- "bold",
72
- "italic",
73
- "strike",
74
- "highlight"
75
- ];
76
70
  function markupTools(styles) {
77
71
  return MARKUP_TOOLS.filter((t) => styles.includes(t.style));
78
72
  }
@@ -82,6 +76,7 @@ var STANDARD_MARKUP = [
82
76
  { open: "~~", close: "~~", kind: "strike" },
83
77
  { open: "_", close: "_", kind: "italic", wordBoundary: true }
84
78
  ];
79
+ var COPY_FIELD_TOOLS = MARKUP_TOOLS.map((tool) => tool.style).filter((style) => STANDARD_MARKUP.some((rule) => rule.kind === style));
85
80
  var STOREFRONT_MARKUP = [
86
81
  ...STANDARD_MARKUP,
87
82
  { open: "*", close: "*", kind: "accent" }
@@ -89,8 +84,8 @@ var STOREFRONT_MARKUP = [
89
84
  var CAMPAIGN_MARKUP = [
90
85
  ...STANDARD_MARKUP,
91
86
  { open: "*", close: "*", kind: "accent" },
92
- { open: "#", close: "#", kind: "bold", notBeforeDigit: true },
93
- { open: "[", close: "]", kind: "italic" }
87
+ { open: "#", close: "#", kind: "bold", notBefore: "0123456789" },
88
+ { open: "[", close: "]", kind: "italic", notBefore: "(" }
94
89
  ];
95
90
  function wordish(ch) {
96
91
  return !/\s/.test(ch) && !/[!-\/:-@[-`{-~]/.test(ch);
@@ -125,7 +120,7 @@ function matchAt(value, i, rules) {
125
120
  if (inner.includes(rule.close[0])) continue;
126
121
  const end = closeAt + rule.close.length;
127
122
  const next = value[end];
128
- if (rule.notBeforeDigit && next !== void 0 && next >= "0" && next <= "9") continue;
123
+ if (rule.notBefore && next !== void 0 && rule.notBefore.includes(next)) continue;
129
124
  if (rule.wordBoundary && next !== void 0 && wordish(next)) continue;
130
125
  return { rule, inner, end };
131
126
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/markup/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The editorial markup rule — ONE grammar, shared by every product that lets a\n * human type formatted copy into a plain text box.\n *\n * Why this lives in react-os-shell, and why behind its own subpath:\n * the rule is consumed by the admin portal (agent messages + the campaign\n * designer) AND by the public storefront, which has ten dependencies in total\n * and cannot afford the shell's peer graph (a 3D viewer, a PDF renderer, a DXF\n * viewer, an xlsx parser, a Word converter). So this module is deliberately\n * FRAMEWORK-FREE: no React, no JSX, no DOM, no imports at all. `react-os-shell/markup`\n * therefore adds a few hundred bytes of first-party code to a bundle and nothing\n * else — see the subpath's own tsup entry.\n *\n * WHAT IS SHARED IS THE RULE, NOT THE RENDERING. Each product renders the token\n * list its own way, because it must: the web uses CSS classes bound to theme\n * tokens, email must inline every style (mail clients drop classes). Two\n * renderers, one grammar, one writer ({@link applyMark}) — so a toolbar button\n * and a parser can never disagree about what a delimiter means.\n *\n * ## The delimiters\n *\n * Standard markdown, in every product:\n *\n * **phrase** bold\n * _phrase_ italic\n * ~~phrase~~ strikethrough\n * ==phrase== highlight — the brand accent colour, applied to a selection\n *\n * Plus a per-product LEGACY set ({@link STOREFRONT_MARKUP} /\n * {@link CAMPAIGN_MARKUP}) that keeps already-published copy rendering exactly\n * as it does today. Those legacy rules are the only part of this grammar that is\n * meant to be deleted: once stored content has been converted, drop the rule\n * from the product's rule list and nothing else changes.\n *\n * ## Two deliberate delimiter choices\n *\n * 1. ITALIC IS `_phrase_`, NOT `*phrase*`. In the storefront and the campaign\n * designer a single asterisk already means the ACCENT colour, on 704 live\n * instances. Standard markdown says italic. Both cannot be true, so the\n * asterisk keeps its existing meaning and italic takes markdown's OTHER\n * italic marker. Nothing published moves.\n * 2. `_` NEVER FIRES INSIDE A WORD. Without that guard, a mail merge line\n * holding `{{first_name}}` and `{{last_name}}` would italicise everything\n * between the two underscores. The guard is CommonMark's own rule for `_`\n * (an intraword underscore is literal) and it is checked with plain character\n * tests rather than a lookbehind, because a lookbehind is a parse error on\n * Safari below 16.4 and this code ships to a public storefront.\n */\n\n/** A style a toolbar button can apply. The first five are inline marks; the\n * last four are the composer-only affordances (a link and the three line\n * prefixes), which only a renderer that draws block content can honour. */\nexport type MarkStyle =\n | 'bold' | 'italic' | 'strike' | 'code' | 'highlight'\n | 'link' | 'bullet' | 'number' | 'quote';\n\n/** What a parsed run MEANS. `accent` is the storefront/campaign brand colour —\n * the legacy `*phrase*` — and `highlight` is its standard-markdown successor;\n * both resolve to the same paint, which is what makes converting stored copy\n * from one to the other invisible. */\nexport type InlineKind =\n | 'text' | 'bold' | 'italic' | 'strike' | 'code' | 'highlight' | 'accent';\n\n// ── Writing: the toolbar's half of the rule ────────────────────────────────\n\nconst INLINE: Record<string, { fence: string; placeholder: string }> = {\n bold: { fence: '**', placeholder: 'bold text' },\n italic: { fence: '_', placeholder: 'italic text' },\n strike: { fence: '~~', placeholder: 'struck out' },\n code: { fence: '`', placeholder: 'code' },\n highlight: { fence: '==', placeholder: 'highlighted text' },\n};\n\nconst LINE_PREFIX: Record<string, (i: number) => string> = {\n bullet: () => '- ',\n number: (i) => `${i + 1}. `,\n quote: () => '> ',\n};\n\nexport interface WrapResult {\n text: string;\n /** Where the caret should land — inside the new marks, or after them. */\n selectionStart: number;\n selectionEnd: number;\n}\n\n/**\n * Apply a style to `text[start:end]`, returning the new text and where the\n * selection should sit. Toggling is deliberate: pressing Bold on already-bold\n * text unwraps it, so the shortcut behaves like every other editor.\n *\n * Unwrapping only ever recognises the style's OWN fence. An italic button that\n * also unwrapped `*phrase*` would silently strip an accent run in the storefront\n * and the campaign designer, where the asterisk means colour, not slant.\n */\nexport function applyMark(\n text: string, start: number, end: number, style: MarkStyle,\n): WrapResult {\n const selected = text.slice(start, end);\n\n if (style === 'link') {\n const label = selected || 'link text';\n const snippet = `[${label}](https://)`;\n return {\n text: text.slice(0, start) + snippet + text.slice(end),\n // Land the caret on the URL — the label is usually already right.\n // Offset spans \"[\", the label, \"](\" — three characters plus the label.\n selectionStart: start + label.length + 3,\n selectionEnd: start + snippet.length - 1,\n };\n }\n\n const prefix = LINE_PREFIX[style];\n if (prefix) {\n // Line styles apply to every line the selection touches.\n const from = text.lastIndexOf('\\n', Math.max(start - 1, 0)) + 1;\n const toIdx = text.indexOf('\\n', end);\n const to = toIdx === -1 ? text.length : toIdx;\n const block = text.slice(from, to);\n const lines = block.split('\\n');\n const marked = lines.every((l, i) => l.startsWith(prefix(i)));\n const next = lines\n .map((l, i) => (marked ? l.slice(prefix(i).length) : prefix(i) + l))\n .join('\\n');\n return {\n text: text.slice(0, from) + next + text.slice(to),\n selectionStart: from,\n selectionEnd: from + next.length,\n };\n }\n\n const { fence, placeholder } = INLINE[style];\n const before = text.slice(Math.max(0, start - fence.length), start);\n const after = text.slice(end, end + fence.length);\n if (before === fence && after === fence) { // already marked → unwrap\n return {\n text: text.slice(0, start - fence.length) + selected + text.slice(end + fence.length),\n selectionStart: start - fence.length,\n selectionEnd: start - fence.length + selected.length,\n };\n }\n const inner = selected || placeholder;\n return {\n text: text.slice(0, start) + fence + inner + fence + text.slice(end),\n selectionStart: start + fence.length,\n selectionEnd: start + fence.length + inner.length,\n };\n}\n\n/** One toolbar button. `label` is a text glyph for hosts that draw type\n * (the message composer); hosts with an icon set map `style` to their own.\n * `shortcut` is the bare key the host combines with its platform modifier. */\nexport interface MarkupTool {\n style: MarkStyle;\n label: string;\n title: string;\n shortcut?: string;\n}\n\n/** Every button, in the order a writer reaches for them. */\nexport const MARKUP_TOOLS: readonly MarkupTool[] = [\n { style: 'bold', label: 'B', title: 'Bold (⌘B)', shortcut: 'b' },\n { style: 'italic', label: 'I', title: 'Italic (⌘I)', shortcut: 'i' },\n { style: 'strike', label: 'S', title: 'Strikethrough' },\n { style: 'highlight', label: '◆', title: 'Highlight in the brand colour' },\n { style: 'code', label: '‹›', title: 'Code' },\n { style: 'link', label: '🔗', title: 'Link (⌘K)', shortcut: 'k' },\n { style: 'bullet', label: '•', title: 'Bulleted list' },\n { style: 'number', label: '1.', title: 'Numbered list' },\n { style: 'quote', label: '❝', title: 'Quote' },\n];\n\n/**\n * The four buttons a COPY FIELD offers — a storefront section's copy box and a\n * campaign block's copy box show exactly these, so a merchant meets one toolbar\n * wherever they type. Listed here rather than per product for the obvious\n * reason: two lists would drift.\n *\n * The other five are absent on purpose. A storefront section and an email block\n * render inline runs only — no lists, no block quotes, no anchor. Offering a\n * button whose output the renderer prints as literal text is worse than not\n * offering it, so a field advertises exactly what its renderer can draw.\n */\nexport const COPY_FIELD_TOOLS: readonly MarkStyle[] = [\n 'bold', 'italic', 'strike', 'highlight',\n];\n\n/** The tools a host shows, resolved to descriptors in {@link MARKUP_TOOLS}\n * order. Keeps a host from hand-rebuilding a descriptor (and drifting on a\n * tooltip) just to pick a subset. */\nexport function markupTools(styles: readonly MarkStyle[]): MarkupTool[] {\n return MARKUP_TOOLS.filter((t) => styles.includes(t.style));\n}\n\n// ── Reading: the parser's half of the rule ─────────────────────────────────\n\n/**\n * One delimited run. `open`/`close` are literal strings; a run's inner text may\n * never contain the first character of `close`, which is what keeps this\n * equivalent to the \"asterisk, run of non-asterisks, asterisk\" regexes the\n * products used before — the same leftmost match, the same single close\n * candidate, so today's stored copy parses exactly as it always has.\n */\nexport interface InlineRule {\n readonly open: string;\n readonly close: string;\n readonly kind: Exclude<InlineKind, 'text'>;\n /** CommonMark's `_` rule: neither delimiter may touch a word character, so\n * `snake_case` and `{{first_name}}` stay literal. */\n readonly wordBoundary?: boolean;\n /** A close followed by a digit does not close, so rank markers (\"Ranked #1\n * and #2\") stay literal instead of pairing up. */\n readonly notBeforeDigit?: boolean;\n}\n\n/**\n * The standard-markdown marks, shared by every product.\n *\n * `**` and `*` cannot collide even though they share a prefix: a `*` followed\n * immediately by another `*` is an EMPTY accent run, which no rule matches, so\n * at any one position exactly one of the two can fire. Order between them is\n * therefore not observable — which is worth stating, because the obvious\n * assumption (\"longest delimiter must be listed first\") would make a future\n * reader think this array's order is load-bearing when the empty-run rule is\n * what actually does the work.\n */\nexport const STANDARD_MARKUP: readonly InlineRule[] = [\n { open: '**', close: '**', kind: 'bold' },\n { open: '==', close: '==', kind: 'highlight' },\n { open: '~~', close: '~~', kind: 'strike' },\n { open: '_', close: '_', kind: 'italic', wordBoundary: true },\n];\n\n/**\n * The storefront's rule set: standard markdown plus the ONE legacy run it has\n * published — `*phrase*` in the theme accent. Delete that last entry once\n * stored copy has been converted to `==phrase==`; both kinds already paint the\n * same, so the deletion is invisible.\n */\nexport const STOREFRONT_MARKUP: readonly InlineRule[] = [\n ...STANDARD_MARKUP,\n { open: '*', close: '*', kind: 'accent' },\n];\n\n/**\n * The campaign designer's rule set: standard markdown plus its three legacy\n * runs — `#phrase#` bold, `*phrase*` accent, `[phrase]` italic. The bold guard\n * is the one the email parser already carried: a `#` followed by a digit does\n * not close a run, so \"Ranked #1 and #2\" is literal while \"Save #20%#\" bolds.\n */\nexport const CAMPAIGN_MARKUP: readonly InlineRule[] = [\n ...STANDARD_MARKUP,\n { open: '*', close: '*', kind: 'accent' },\n { open: '#', close: '#', kind: 'bold', notBeforeDigit: true },\n { open: '[', close: ']', kind: 'italic' },\n];\n\n/** A parsed run. `text` is the inner text with the delimiters removed, so a\n * caller that only wants words ({@link stripInline}) joins the texts and a\n * caller that paints ({@link InlineKind}) switches on the kind. */\nexport interface InlineToken {\n kind: InlineKind;\n text: string;\n}\n\n/** True for a character that a `_` may not touch. Whitespace and ASCII\n * punctuation are safe neighbours; everything else — letters, digits, and any\n * non-ASCII script — counts as part of a word, so `汉_字_` stays literal too.\n * Deliberately conservative: a character wrongly called word-ish only ever\n * leaves an underscore rendering as it does today. */\nfunction wordish(ch: string): boolean {\n return !/\\s/.test(ch) && !/[!-\\/:-@[-`{-~]/.test(ch);\n}\n\n/**\n * Split authored text into its runs.\n *\n * The returned tokens ALTERNATE, starting and ending with a `text` token that\n * may be empty — exactly the shape `String.split` with one capture group\n * produced in the parsers this replaces. That contract is load-bearing, not\n * cosmetic: the storefront wraps every segment (including the empty ones) in a\n * `<span>`, so preserving the empty tokens is what keeps a published page's\n * markup byte-for-byte identical.\n *\n * A lone delimiter with no partner stays literal — the products' copy contains\n * real stray asterisks, and swallowing one would silently eat a word.\n */\nexport function tokenizeInline(\n value: string, rules: readonly InlineRule[],\n): InlineToken[] {\n const out: InlineToken[] = [];\n let buf = '';\n let i = 0;\n\n while (i < value.length) {\n const hit = matchAt(value, i, rules);\n if (hit) {\n out.push({ kind: 'text', text: buf });\n buf = '';\n out.push({ kind: hit.rule.kind, text: hit.inner });\n i = hit.end;\n continue;\n }\n buf += value[i];\n i += 1;\n }\n out.push({ kind: 'text', text: buf });\n return out;\n}\n\n/** The first rule that opens at `i` AND finds its close, or null. */\nfunction matchAt(value: string, i: number, rules: readonly InlineRule[]) {\n for (const rule of rules) {\n if (!value.startsWith(rule.open, i)) continue;\n if (rule.wordBoundary && i > 0 && wordish(value[i - 1])) continue;\n\n const from = i + rule.open.length;\n const closeAt = value.indexOf(rule.close, from);\n if (closeAt < from + 1) continue; // no close, or an empty run\n\n // The inner text may not contain the close's leading character: one close\n // candidate per opening, which is what the regexes this replaces did.\n const inner = value.slice(from, closeAt);\n if (inner.includes(rule.close[0])) continue;\n\n const end = closeAt + rule.close.length;\n const next = value[end];\n if (rule.notBeforeDigit && next !== undefined && next >= '0' && next <= '9') continue;\n if (rule.wordBoundary && next !== undefined && wordish(next)) continue;\n\n return { rule, inner, end };\n }\n return null;\n}\n\n/**\n * The plain words of an authored string — every paired run unwrapped to its\n * inner text, every unpaired delimiter left alone.\n *\n * This is the ONLY correct way to fill an `alt`, an `aria-label` or a\n * structured-data field from authored copy. Deleting a delimiter character by\n * hand — \"replace every asterisk with nothing\" — breaks the moment the grammar\n * grows a marker, and it disagrees with the renderer about a stray asterisk,\n * which matters: search engines expect structured data to match the text a\n * visitor actually sees.\n */\nexport function stripInline(value: string, rules: readonly InlineRule[]): string {\n return tokenizeInline(value, rules).map((t) => t.text).join('');\n}\n"]}
1
+ {"version":3,"sources":["../../src/markup/index.ts"],"names":[],"mappings":";AA2FA,IAAM,MAAA,GAAiE;AAAA,EACrE,IAAA,EAAM,EAAE,KAAA,EAAO,IAAA,EAAM,aAAa,WAAA,EAAY;AAAA,EAC9C,MAAA,EAAQ,EAAE,KAAA,EAAO,GAAA,EAAK,aAAa,aAAA,EAAc;AAAA,EACjD,MAAA,EAAQ,EAAE,KAAA,EAAO,IAAA,EAAM,aAAa,YAAA,EAAa;AAAA,EACjD,IAAA,EAAM,EAAE,KAAA,EAAO,GAAA,EAAK,aAAa,MAAA,EAAO;AAAA,EACxC,SAAA,EAAW,EAAE,KAAA,EAAO,IAAA,EAAM,aAAa,kBAAA;AACzC,CAAA;AAEA,IAAM,WAAA,GAAqD;AAAA,EACzD,QAAQ,MAAM,IAAA;AAAA,EACd,MAAA,EAAQ,CAAC,CAAA,KAAM,CAAA,EAAG,IAAI,CAAC,CAAA,EAAA,CAAA;AAAA,EACvB,OAAO,MAAM;AACf,CAAA;AAkBO,SAAS,SAAA,CACd,IAAA,EAAc,KAAA,EAAe,GAAA,EAAa,KAAA,EAC9B;AACZ,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,GAAG,CAAA;AAEtC,EAAA,IAAI,UAAU,MAAA,EAAQ;AACpB,IAAA,MAAM,QAAQ,QAAA,IAAY,WAAA;AAC1B,IAAA,MAAM,OAAA,GAAU,IAAI,KAAK,CAAA,WAAA,CAAA;AACzB,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,KAAK,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA,GAAI,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAAA;AAAA;AAAA,MAGrD,cAAA,EAAgB,KAAA,GAAQ,KAAA,CAAM,MAAA,GAAS,CAAA;AAAA,MACvC,YAAA,EAAc,KAAA,GAAQ,OAAA,CAAQ,MAAA,GAAS;AAAA,KACzC;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,YAAY,KAAK,CAAA;AAChC,EAAA,IAAI,MAAA,EAAQ;AAEV,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,WAAA,CAAY,IAAA,EAAM,IAAA,CAAK,IAAI,KAAA,GAAQ,CAAA,EAAG,CAAC,CAAC,CAAA,GAAI,CAAA;AAC9D,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AACpC,IAAA,MAAM,EAAA,GAAK,KAAA,KAAU,EAAA,GAAK,IAAA,CAAK,MAAA,GAAS,KAAA;AACxC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,EAAE,CAAA;AACjC,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAC9B,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,UAAA,CAAW,MAAA,CAAO,CAAC,CAAC,CAAC,CAAA;AAC5D,IAAA,MAAM,IAAA,GAAO,MACV,GAAA,CAAI,CAAC,GAAG,CAAA,KAAO,MAAA,GAAS,EAAE,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,CAAE,MAAM,IAAI,MAAA,CAAO,CAAC,IAAI,CAAE,CAAA,CAClE,KAAK,IAAI,CAAA;AACZ,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,KAAK,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,GAAI,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,EAAE,CAAA;AAAA,MAChD,cAAA,EAAgB,IAAA;AAAA,MAChB,YAAA,EAAc,OAAO,IAAA,CAAK;AAAA,KAC5B;AAAA,EACF;AAEA,EAAA,MAAM,EAAE,KAAA,EAAO,WAAA,EAAY,GAAI,OAAO,KAAK,CAAA;AAC3C,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,GAAG,KAAA,GAAQ,KAAA,CAAM,MAAM,CAAA,EAAG,KAAK,CAAA;AAClE,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,EAAK,GAAA,GAAM,MAAM,MAAM,CAAA;AAChD,EAAA,IAAI,MAAA,KAAW,KAAA,IAAS,KAAA,KAAU,KAAA,EAAO;AACvC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,KAAA,GAAQ,KAAA,CAAM,MAAM,CAAA,GAAI,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,MAAM,MAAM,CAAA;AAAA,MACpF,cAAA,EAAgB,QAAQ,KAAA,CAAM,MAAA;AAAA,MAC9B,YAAA,EAAc,KAAA,GAAQ,KAAA,CAAM,MAAA,GAAS,QAAA,CAAS;AAAA,KAChD;AAAA,EACF;AACA,EAAA,MAAM,QAAQ,QAAA,IAAY,WAAA;AAC1B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA,GAAI,KAAA,GAAQ,KAAA,GAAQ,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAAA,IACnE,cAAA,EAAgB,QAAQ,KAAA,CAAM,MAAA;AAAA,IAC9B,YAAA,EAAc,KAAA,GAAQ,KAAA,CAAM,MAAA,GAAS,KAAA,CAAM;AAAA,GAC7C;AACF;AAaO,IAAM,YAAA,GAAsC;AAAA,EACjD,EAAE,OAAO,MAAA,EAAQ,KAAA,EAAO,KAAK,KAAA,EAAO,gBAAA,EAAa,UAAU,GAAA,EAAI;AAAA,EAC/D,EAAE,OAAO,QAAA,EAAU,KAAA,EAAO,KAAK,KAAA,EAAO,kBAAA,EAAe,UAAU,GAAA,EAAI;AAAA,EACnE,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,GAAA,EAAK,OAAO,eAAA,EAAgB;AAAA,EACtD,EAAE,KAAA,EAAO,WAAA,EAAa,KAAA,EAAO,QAAA,EAAK,OAAO,+BAAA,EAAgC;AAAA,EACzE,EAAE,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAO,cAAA,EAAM,OAAO,MAAA,EAAO;AAAA,EAC5C,EAAE,OAAO,MAAA,EAAQ,KAAA,EAAO,aAAM,KAAA,EAAO,gBAAA,EAAa,UAAU,GAAA,EAAI;AAAA,EAChE,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,QAAA,EAAK,OAAO,eAAA,EAAgB;AAAA,EACtD,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,IAAA,EAAM,OAAO,eAAA,EAAgB;AAAA,EACvD,EAAE,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,QAAA,EAAK,OAAO,OAAA;AACvC;AAKO,SAAS,YAAY,MAAA,EAA4C;AACtE,EAAA,OAAO,YAAA,CAAa,OAAO,CAAC,CAAA,KAAM,OAAO,QAAA,CAAS,CAAA,CAAE,KAAK,CAAC,CAAA;AAC5D;AA8CO,IAAM,eAAA,GAAyC;AAAA,EACpD,EAAE,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,MAAM,MAAA,EAAO;AAAA,EACxC,EAAE,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,MAAM,WAAA,EAAY;AAAA,EAC7C,EAAE,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,MAAM,QAAA,EAAS;AAAA,EAC1C,EAAE,MAAM,GAAA,EAAK,KAAA,EAAO,KAAK,IAAA,EAAM,QAAA,EAAU,cAAc,IAAA;AACzD;AAsBO,IAAM,mBAAyC,YAAA,CACnD,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,KAAK,CAAA,CACxB,MAAA,CAAO,CAAC,KAAA,KAAU,gBAAgB,IAAA,CAAK,CAAC,SAAS,IAAA,CAAK,IAAA,KAAS,KAAK,CAAC;AAQjE,IAAM,iBAAA,GAA2C;AAAA,EACtD,GAAG,eAAA;AAAA,EACH,EAAE,IAAA,EAAM,GAAA,EAAK,KAAA,EAAO,GAAA,EAAK,MAAM,QAAA;AACjC;AAiBO,IAAM,eAAA,GAAyC;AAAA,EACpD,GAAG,eAAA;AAAA,EACH,EAAE,IAAA,EAAM,GAAA,EAAK,KAAA,EAAO,GAAA,EAAK,MAAM,QAAA,EAAS;AAAA,EACxC,EAAE,MAAM,GAAA,EAAK,KAAA,EAAO,KAAK,IAAA,EAAM,MAAA,EAAQ,WAAW,YAAA,EAAa;AAAA,EAC/D,EAAE,MAAM,GAAA,EAAK,KAAA,EAAO,KAAK,IAAA,EAAM,QAAA,EAAU,WAAW,GAAA;AACtD;AAeA,SAAS,QAAQ,EAAA,EAAqB;AACpC,EAAA,OAAO,CAAC,KAAK,IAAA,CAAK,EAAE,KAAK,CAAC,iBAAA,CAAkB,KAAK,EAAE,CAAA;AACrD;AAeO,SAAS,cAAA,CACd,OAAe,KAAA,EACA;AACf,EAAA,MAAM,MAAqB,EAAC;AAC5B,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,CAAA,GAAI,CAAA;AAER,EAAA,OAAO,CAAA,GAAI,MAAM,MAAA,EAAQ;AACvB,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,EAAO,CAAA,EAAG,KAAK,CAAA;AACnC,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,GAAA,CAAI,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,KAAK,CAAA;AACpC,MAAA,GAAA,GAAM,EAAA;AACN,MAAA,GAAA,CAAI,IAAA,CAAK,EAAE,IAAA,EAAM,GAAA,CAAI,KAAK,IAAA,EAAM,IAAA,EAAM,GAAA,CAAI,KAAA,EAAO,CAAA;AACjD,MAAA,CAAA,GAAI,GAAA,CAAI,GAAA;AACR,MAAA;AAAA,IACF;AACA,IAAA,GAAA,IAAO,MAAM,CAAC,CAAA;AACd,IAAA,CAAA,IAAK,CAAA;AAAA,EACP;AACA,EAAA,GAAA,CAAI,KAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,KAAK,CAAA;AACpC,EAAA,OAAO,GAAA;AACT;AAGA,SAAS,OAAA,CAAQ,KAAA,EAAe,CAAA,EAAW,KAAA,EAA8B;AACvE,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,CAAC,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,IAAA,EAAM,CAAC,CAAA,EAAG;AACrC,IAAA,IAAI,IAAA,CAAK,gBAAgB,CAAA,GAAI,CAAA,IAAK,QAAQ,KAAA,CAAM,CAAA,GAAI,CAAC,CAAC,CAAA,EAAG;AAEzD,IAAA,MAAM,IAAA,GAAO,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,MAAA;AAC3B,IAAA,MAAM,OAAA,GAAU,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,OAAO,IAAI,CAAA;AAC9C,IAAA,IAAI,OAAA,GAAU,OAAO,CAAA,EAAG;AAIxB,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAA,EAAM,OAAO,CAAA;AACvC,IAAA,IAAI,MAAM,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA,EAAG;AAEnC,IAAA,MAAM,GAAA,GAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,MAAA;AACjC,IAAA,MAAM,IAAA,GAAO,MAAM,GAAG,CAAA;AACtB,IAAA,IAAI,IAAA,CAAK,aAAa,IAAA,KAAS,MAAA,IAAa,KAAK,SAAA,CAAU,QAAA,CAAS,IAAI,CAAA,EAAG;AAC3E,IAAA,IAAI,KAAK,YAAA,IAAgB,IAAA,KAAS,MAAA,IAAa,OAAA,CAAQ,IAAI,CAAA,EAAG;AAE9D,IAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,GAAA,EAAI;AAAA,EAC5B;AACA,EAAA,OAAO,IAAA;AACT;AAaO,SAAS,WAAA,CAAY,OAAe,KAAA,EAAsC;AAC/E,EAAA,OAAO,cAAA,CAAe,KAAA,EAAO,KAAK,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA;AAChE","file":"index.js","sourcesContent":["/**\n * The editorial markup rule — ONE grammar, shared by every product that lets a\n * human type formatted copy into a plain text box.\n *\n * Why this lives in react-os-shell, and why behind its own subpath:\n * the rule is consumed by the admin portal (agent messages + the campaign\n * designer) AND by the public storefront, which has ten dependencies in total\n * and cannot afford the shell's peer graph (a 3D viewer, a PDF renderer, a DXF\n * viewer, an xlsx parser, a Word converter). So this module is deliberately\n * FRAMEWORK-FREE: no React, no JSX, no DOM, no imports at all. `react-os-shell/markup`\n * therefore adds a few hundred bytes of first-party code to a bundle and nothing\n * else — see the subpath's own tsup entry.\n *\n * It is also the ONE subpath whose `exports` entry carries a `default`\n * condition alongside `import`, and that is deliberate rather than untidy. The\n * root and `./apps` are React component entries; a bundler resolves them and\n * nothing else has any business to. This module is consumed by build scripts\n * too — the storefront's media-manifest generator reaches it through its Puck\n * config — and those run under `tsx`, which resolves through Node's CJS\n * loader. With only `types` and `import` declared, that resolution fails with\n * ERR_PACKAGE_PATH_NOT_EXPORTED, which reads like a missing file rather than a\n * missing condition. A module this portable should answer to any resolver.\n *\n * WHAT IS SHARED IS THE RULE, NOT THE RENDERING. Each product renders the token\n * list its own way, because it must: the web uses CSS classes bound to theme\n * tokens, email must inline every style (mail clients drop classes). Two\n * renderers, one grammar, one writer ({@link applyMark}) — so a toolbar button\n * and a parser can never disagree about what a delimiter means.\n *\n * ## The delimiters\n *\n * Standard markdown, in every product:\n *\n * **phrase** bold\n * _phrase_ italic\n * ~~phrase~~ strikethrough\n * ==phrase== highlight — the brand accent colour, applied to a selection\n *\n * Plus a per-product LEGACY set ({@link STOREFRONT_MARKUP} /\n * {@link CAMPAIGN_MARKUP}) that keeps already-published copy rendering exactly\n * as it does today. Those legacy rules are the only part of this grammar that is\n * meant to be deleted: once stored content has been converted, drop the rule\n * from the product's rule list and nothing else changes.\n *\n * ## Two deliberate delimiter choices\n *\n * 1. ITALIC IS `_phrase_`, NOT `*phrase*`. In the storefront and the campaign\n * designer a single asterisk already means the ACCENT colour, on 704 live\n * instances. Standard markdown says italic. Both cannot be true, so the\n * asterisk keeps its existing meaning and italic takes markdown's OTHER\n * italic marker. Nothing published moves.\n * 2. `_` NEVER FIRES INSIDE A WORD. Without that guard, a mail merge line\n * holding `{{first_name}}` and `{{last_name}}` would italicise everything\n * between the two underscores. The guard is CommonMark's own rule for `_`\n * (an intraword underscore is literal) and it is checked with plain character\n * tests rather than a lookbehind, because a lookbehind is a parse error on\n * Safari below 16.4 and this code ships to a public storefront.\n *\n * ## One KNOWN divergence from the standard named above\n *\n * `__phrase__` IS ITALIC HERE. CommonMark reads a doubled underscore as STRONG;\n * this grammar has a single `_` rule, so the outer pair matches, the inner\n * underscores are ordinary characters, and `__all__` comes back as `_all_`.\n *\n * Left alone deliberately, and recorded rather than fixed. A census of stored\n * copy found 14 rows in this shape and every one of them is the literal link\n * key `__all__`, which no renderer is handed — so nothing published is being\n * mis-drawn today. Adding a `__` rule to buy CommonMark parity would change how\n * a stored `_` pair parses for the benefit of no existing string, and this\n * module's promise is that the grammar grows while nothing published moves.\n * Bold is `**phrase**` in every product; that is the form the toolbar writes\n * and the form to reach for. Pinned by a test, so it stays a decision rather\n * than becoming a discovery.\n */\n\n/** A style a toolbar button can apply. The first five are inline marks; the\n * last four are the composer-only affordances (a link and the three line\n * prefixes), which only a renderer that draws block content can honour. */\nexport type MarkStyle =\n | 'bold' | 'italic' | 'strike' | 'code' | 'highlight'\n | 'link' | 'bullet' | 'number' | 'quote';\n\n/** What a parsed run MEANS. `accent` is the storefront/campaign brand colour —\n * the legacy `*phrase*` — and `highlight` is its standard-markdown successor;\n * both resolve to the same paint, which is what makes converting stored copy\n * from one to the other invisible. */\nexport type InlineKind =\n | 'text' | 'bold' | 'italic' | 'strike' | 'code' | 'highlight' | 'accent';\n\n// ── Writing: the toolbar's half of the rule ────────────────────────────────\n\nconst INLINE: Record<string, { fence: string; placeholder: string }> = {\n bold: { fence: '**', placeholder: 'bold text' },\n italic: { fence: '_', placeholder: 'italic text' },\n strike: { fence: '~~', placeholder: 'struck out' },\n code: { fence: '`', placeholder: 'code' },\n highlight: { fence: '==', placeholder: 'highlighted text' },\n};\n\nconst LINE_PREFIX: Record<string, (i: number) => string> = {\n bullet: () => '- ',\n number: (i) => `${i + 1}. `,\n quote: () => '> ',\n};\n\nexport interface WrapResult {\n text: string;\n /** Where the caret should land — inside the new marks, or after them. */\n selectionStart: number;\n selectionEnd: number;\n}\n\n/**\n * Apply a style to `text[start:end]`, returning the new text and where the\n * selection should sit. Toggling is deliberate: pressing Bold on already-bold\n * text unwraps it, so the shortcut behaves like every other editor.\n *\n * Unwrapping only ever recognises the style's OWN fence. An italic button that\n * also unwrapped `*phrase*` would silently strip an accent run in the storefront\n * and the campaign designer, where the asterisk means colour, not slant.\n */\nexport function applyMark(\n text: string, start: number, end: number, style: MarkStyle,\n): WrapResult {\n const selected = text.slice(start, end);\n\n if (style === 'link') {\n const label = selected || 'link text';\n const snippet = `[${label}](https://)`;\n return {\n text: text.slice(0, start) + snippet + text.slice(end),\n // Land the caret on the URL — the label is usually already right.\n // Offset spans \"[\", the label, \"](\" — three characters plus the label.\n selectionStart: start + label.length + 3,\n selectionEnd: start + snippet.length - 1,\n };\n }\n\n const prefix = LINE_PREFIX[style];\n if (prefix) {\n // Line styles apply to every line the selection touches.\n const from = text.lastIndexOf('\\n', Math.max(start - 1, 0)) + 1;\n const toIdx = text.indexOf('\\n', end);\n const to = toIdx === -1 ? text.length : toIdx;\n const block = text.slice(from, to);\n const lines = block.split('\\n');\n const marked = lines.every((l, i) => l.startsWith(prefix(i)));\n const next = lines\n .map((l, i) => (marked ? l.slice(prefix(i).length) : prefix(i) + l))\n .join('\\n');\n return {\n text: text.slice(0, from) + next + text.slice(to),\n selectionStart: from,\n selectionEnd: from + next.length,\n };\n }\n\n const { fence, placeholder } = INLINE[style];\n const before = text.slice(Math.max(0, start - fence.length), start);\n const after = text.slice(end, end + fence.length);\n if (before === fence && after === fence) { // already marked → unwrap\n return {\n text: text.slice(0, start - fence.length) + selected + text.slice(end + fence.length),\n selectionStart: start - fence.length,\n selectionEnd: start - fence.length + selected.length,\n };\n }\n const inner = selected || placeholder;\n return {\n text: text.slice(0, start) + fence + inner + fence + text.slice(end),\n selectionStart: start + fence.length,\n selectionEnd: start + fence.length + inner.length,\n };\n}\n\n/** One toolbar button. `label` is a text glyph for hosts that draw type\n * (the message composer); hosts with an icon set map `style` to their own.\n * `shortcut` is the bare key the host combines with its platform modifier. */\nexport interface MarkupTool {\n style: MarkStyle;\n label: string;\n title: string;\n shortcut?: string;\n}\n\n/** Every button, in the order a writer reaches for them. */\nexport const MARKUP_TOOLS: readonly MarkupTool[] = [\n { style: 'bold', label: 'B', title: 'Bold (⌘B)', shortcut: 'b' },\n { style: 'italic', label: 'I', title: 'Italic (⌘I)', shortcut: 'i' },\n { style: 'strike', label: 'S', title: 'Strikethrough' },\n { style: 'highlight', label: '◆', title: 'Highlight in the brand colour' },\n { style: 'code', label: '‹›', title: 'Code' },\n { style: 'link', label: '🔗', title: 'Link (⌘K)', shortcut: 'k' },\n { style: 'bullet', label: '•', title: 'Bulleted list' },\n { style: 'number', label: '1.', title: 'Numbered list' },\n { style: 'quote', label: '❝', title: 'Quote' },\n];\n\n/** The tools a host shows, resolved to descriptors in {@link MARKUP_TOOLS}\n * order. Keeps a host from hand-rebuilding a descriptor (and drifting on a\n * tooltip) just to pick a subset. */\nexport function markupTools(styles: readonly MarkStyle[]): MarkupTool[] {\n return MARKUP_TOOLS.filter((t) => styles.includes(t.style));\n}\n\n// ── Reading: the parser's half of the rule ─────────────────────────────────\n\n/**\n * One delimited run. `open`/`close` are literal strings; a run's inner text may\n * never contain the first character of `close`, which is what keeps this\n * equivalent to the \"asterisk, run of non-asterisks, asterisk\" regexes the\n * products used before — the same leftmost match, the same single close\n * candidate, so today's stored copy parses exactly as it always has.\n */\nexport interface InlineRule {\n readonly open: string;\n readonly close: string;\n readonly kind: Exclude<InlineKind, 'text'>;\n /** CommonMark's `_` rule: neither delimiter may touch a word character, so\n * `snake_case` and `{{first_name}}` stay literal. */\n readonly wordBoundary?: boolean;\n /**\n * Characters that may not FOLLOW the close. A run whose closing delimiter\n * butts up against one of these does not fire, and both delimiters stay\n * literal.\n *\n * Two legacy rules need it, for the same reason: a delimiter that is also\n * ordinary punctuation will otherwise claim text that was never markup. `#`\n * must not pair across rank markers (\"Ranked #1 and #2\"), and `[` must not\n * claim the label of a markdown link (\"[terms](https://…)\"), where the URL\n * would be stranded outside the run and printed to the reader.\n *\n * A character SET rather than a flag per case, so the next collision is one\n * more rule field and no new branch in {@link tokenizeInline}.\n */\n readonly notBefore?: string;\n}\n\n/**\n * The standard-markdown marks, shared by every product.\n *\n * `**` and `*` cannot collide even though they share a prefix: a `*` followed\n * immediately by another `*` is an EMPTY accent run, which no rule matches, so\n * at any one position exactly one of the two can fire. Order between them is\n * therefore not observable — which is worth stating, because the obvious\n * assumption (\"longest delimiter must be listed first\") would make a future\n * reader think this array's order is load-bearing when the empty-run rule is\n * what actually does the work.\n */\nexport const STANDARD_MARKUP: readonly InlineRule[] = [\n { open: '**', close: '**', kind: 'bold' },\n { open: '==', close: '==', kind: 'highlight' },\n { open: '~~', close: '~~', kind: 'strike' },\n { open: '_', close: '_', kind: 'italic', wordBoundary: true },\n];\n\n/**\n * The buttons a COPY FIELD offers — a storefront section's copy box and a\n * campaign block's copy box show exactly these, so a merchant meets one toolbar\n * wherever they type.\n *\n * DERIVED, not listed. A copy field renders inline runs only — no lists, no\n * block quote, no anchor — so the set it may advertise is precisely the tools\n * whose output this grammar parses back. Writing that as a literal list was the\n * bug's hiding place: the list and the grammar were two hand-maintained facts\n * that agreed by luck, and nothing failed when a tool wrote a delimiter the\n * parser read as something else. Now a style with no rule cannot appear here,\n * whatever anyone types into the array.\n *\n * It resolves to bold, italic, strike and highlight. `code` is the one that\n * surprises: it is an inline mark and it has a fence, but no rule reads a\n * backtick, so a copy field would print `` `word` `` verbatim — and a button\n * whose output the renderer draws as literal text is worse than no button.\n *\n * Declared here, below the rules, because it now depends on them.\n */\nexport const COPY_FIELD_TOOLS: readonly MarkStyle[] = MARKUP_TOOLS\n .map((tool) => tool.style)\n .filter((style) => STANDARD_MARKUP.some((rule) => rule.kind === style));\n\n/**\n * The storefront's rule set: standard markdown plus the ONE legacy run it has\n * published — `*phrase*` in the theme accent. Delete that last entry once\n * stored copy has been converted to `==phrase==`; both kinds already paint the\n * same, so the deletion is invisible.\n */\nexport const STOREFRONT_MARKUP: readonly InlineRule[] = [\n ...STANDARD_MARKUP,\n { open: '*', close: '*', kind: 'accent' },\n];\n\n/**\n * The campaign designer's rule set: standard markdown plus its three legacy\n * runs — `#phrase#` bold, `*phrase*` accent, `[phrase]` italic. The bold guard\n * is the one the email parser already carried: a `#` followed by a digit does\n * not close a run, so \"Ranked #1 and #2\" is literal while \"Save #20%#\" bolds.\n *\n * The italic guard exists because `[phrase]` collides with the one piece of\n * markdown syntax every writer already knows. Without it, \"Read [our\n * terms](https://…) first\" italicises the label AND prints the bare URL to the\n * reader — the words come back wearing a mark nobody chose. It is not a\n * toolbar problem: a merchant typing an ordinary link into a copy box hits it,\n * which is why the guard is on the RULE and not on which buttons a field\n * offers. `[phrase]` on its own is untouched, so every published line still\n * reads exactly as it did.\n */\nexport const CAMPAIGN_MARKUP: readonly InlineRule[] = [\n ...STANDARD_MARKUP,\n { open: '*', close: '*', kind: 'accent' },\n { open: '#', close: '#', kind: 'bold', notBefore: '0123456789' },\n { open: '[', close: ']', kind: 'italic', notBefore: '(' },\n];\n\n/** A parsed run. `text` is the inner text with the delimiters removed, so a\n * caller that only wants words ({@link stripInline}) joins the texts and a\n * caller that paints ({@link InlineKind}) switches on the kind. */\nexport interface InlineToken {\n kind: InlineKind;\n text: string;\n}\n\n/** True for a character that a `_` may not touch. Whitespace and ASCII\n * punctuation are safe neighbours; everything else — letters, digits, and any\n * non-ASCII script — counts as part of a word, so `汉_字_` stays literal too.\n * Deliberately conservative: a character wrongly called word-ish only ever\n * leaves an underscore rendering as it does today. */\nfunction wordish(ch: string): boolean {\n return !/\\s/.test(ch) && !/[!-\\/:-@[-`{-~]/.test(ch);\n}\n\n/**\n * Split authored text into its runs.\n *\n * The returned tokens ALTERNATE, starting and ending with a `text` token that\n * may be empty — exactly the shape `String.split` with one capture group\n * produced in the parsers this replaces. That contract is load-bearing, not\n * cosmetic: the storefront wraps every segment (including the empty ones) in a\n * `<span>`, so preserving the empty tokens is what keeps a published page's\n * markup byte-for-byte identical.\n *\n * A lone delimiter with no partner stays literal — the products' copy contains\n * real stray asterisks, and swallowing one would silently eat a word.\n */\nexport function tokenizeInline(\n value: string, rules: readonly InlineRule[],\n): InlineToken[] {\n const out: InlineToken[] = [];\n let buf = '';\n let i = 0;\n\n while (i < value.length) {\n const hit = matchAt(value, i, rules);\n if (hit) {\n out.push({ kind: 'text', text: buf });\n buf = '';\n out.push({ kind: hit.rule.kind, text: hit.inner });\n i = hit.end;\n continue;\n }\n buf += value[i];\n i += 1;\n }\n out.push({ kind: 'text', text: buf });\n return out;\n}\n\n/** The first rule that opens at `i` AND finds its close, or null. */\nfunction matchAt(value: string, i: number, rules: readonly InlineRule[]) {\n for (const rule of rules) {\n if (!value.startsWith(rule.open, i)) continue;\n if (rule.wordBoundary && i > 0 && wordish(value[i - 1])) continue;\n\n const from = i + rule.open.length;\n const closeAt = value.indexOf(rule.close, from);\n if (closeAt < from + 1) continue; // no close, or an empty run\n\n // The inner text may not contain the close's leading character: one close\n // candidate per opening, which is what the regexes this replaces did.\n const inner = value.slice(from, closeAt);\n if (inner.includes(rule.close[0])) continue;\n\n const end = closeAt + rule.close.length;\n const next = value[end];\n if (rule.notBefore && next !== undefined && rule.notBefore.includes(next)) continue;\n if (rule.wordBoundary && next !== undefined && wordish(next)) continue;\n\n return { rule, inner, end };\n }\n return null;\n}\n\n/**\n * The plain words of an authored string — every paired run unwrapped to its\n * inner text, every unpaired delimiter left alone.\n *\n * This is the ONLY correct way to fill an `alt`, an `aria-label` or a\n * structured-data field from authored copy. Deleting a delimiter character by\n * hand — \"replace every asterisk with nothing\" — breaks the moment the grammar\n * grows a marker, and it disagrees with the renderer about a stray asterisk,\n * which matters: search engines expect structured data to match the text a\n * visitor actually sees.\n */\nexport function stripInline(value: string, rules: readonly InlineRule[]): string {\n return tokenizeInline(value, rules).map((t) => t.text).join('');\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-os-shell",
3
- "version": "4.7.1",
3
+ "version": "4.8.0",
4
4
  "description": "Desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted glass theming, and bundled apps.",
5
5
  "license": "MIT",
6
6
  "author": "Victor Y. Mau",
@@ -30,7 +30,8 @@
30
30
  },
31
31
  "./markup": {
32
32
  "types": "./dist/markup/index.d.ts",
33
- "import": "./dist/markup/index.js"
33
+ "import": "./dist/markup/index.js",
34
+ "default": "./dist/markup/index.js"
34
35
  },
35
36
  "./styles.css": "./dist/styles.css",
36
37
  "./themes.css": "./dist/themes.css"
@@ -1,7 +0,0 @@
1
- // src/version.ts
2
- var VERSION = "4.7.1" ;
3
- var APP_VERSION = VERSION;
4
-
5
- export { APP_VERSION, VERSION };
6
- //# sourceMappingURL=chunk-I465ENNB.js.map
7
- //# sourceMappingURL=chunk-I465ENNB.js.map