react-os-shell 4.6.0 → 4.7.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.
- package/README.md +36 -0
- package/dist/{Browser-MHYO7DQ5.js → Browser-3HL2X4ZO.js} +5 -5
- package/dist/{Browser-MHYO7DQ5.js.map → Browser-3HL2X4ZO.js.map} +1 -1
- package/dist/{Calculator-GBC3FLAJ.js → Calculator-APBKAUZ5.js} +4 -4
- package/dist/{Calculator-GBC3FLAJ.js.map → Calculator-APBKAUZ5.js.map} +1 -1
- package/dist/ConfirmDialog-LO5WHLMD.js +4 -0
- package/dist/{ConfirmDialog-R7NZVI2V.js.map → ConfirmDialog-LO5WHLMD.js.map} +1 -1
- package/dist/{CurrencyConverter-URPZQBNV.js → CurrencyConverter-YOWLTKY7.js} +4 -4
- package/dist/{CurrencyConverter-URPZQBNV.js.map → CurrencyConverter-YOWLTKY7.js.map} +1 -1
- package/dist/{Documents-PKUJIZTP.js → Documents-5NUIGRZX.js} +5 -5
- package/dist/{Documents-PKUJIZTP.js.map → Documents-5NUIGRZX.js.map} +1 -1
- package/dist/{Files-7SMNNOVR.js → Files-2ARKRLBF.js} +5 -5
- package/dist/{Files-7SMNNOVR.js.map → Files-2ARKRLBF.js.map} +1 -1
- package/dist/{Notepad-NMUPPUN6.js → Notepad-U3EZVFEQ.js} +5 -5
- package/dist/{Notepad-NMUPPUN6.js.map → Notepad-U3EZVFEQ.js.map} +1 -1
- package/dist/{PomodoroTimer-QQOMBVP3.js → PomodoroTimer-NA3LPTTY.js} +5 -5
- package/dist/{PomodoroTimer-QQOMBVP3.js.map → PomodoroTimer-NA3LPTTY.js.map} +1 -1
- package/dist/{Preview-TMZH2JJ6.js → Preview-INTAF6NF.js} +5 -5
- package/dist/{Preview-TMZH2JJ6.js.map → Preview-INTAF6NF.js.map} +1 -1
- package/dist/{Spreadsheet-NBB33LYD.js → Spreadsheet-F2CWMS3I.js} +5 -5
- package/dist/{Spreadsheet-NBB33LYD.js.map → Spreadsheet-F2CWMS3I.js.map} +1 -1
- package/dist/{Stock-33C4U7CR.js → Stock-OUVV3A7U.js} +4 -4
- package/dist/{Stock-33C4U7CR.js.map → Stock-OUVV3A7U.js.map} +1 -1
- package/dist/{Weather-G3XHTB75.js → Weather-INO44ZWL.js} +4 -4
- package/dist/{Weather-G3XHTB75.js.map → Weather-INO44ZWL.js.map} +1 -1
- package/dist/{WorldClock-ZUVI3LBN.js → WorldClock-K5IOWZ62.js} +4 -4
- package/dist/{WorldClock-ZUVI3LBN.js.map → WorldClock-K5IOWZ62.js.map} +1 -1
- package/dist/apps/index.js +16 -16
- package/dist/{chunk-LSFZJZTQ.js → chunk-2Y6XUY65.js} +3 -3
- package/dist/{chunk-LSFZJZTQ.js.map → chunk-2Y6XUY65.js.map} +1 -1
- package/dist/{chunk-EF5W3EXL.js → chunk-CKY62ZRU.js} +32 -14
- package/dist/chunk-CKY62ZRU.js.map +1 -0
- package/dist/{chunk-YEO7YRY7.js → chunk-IGZG62LQ.js} +4 -4
- package/dist/{chunk-YEO7YRY7.js.map → chunk-IGZG62LQ.js.map} +1 -1
- package/dist/{chunk-5HCDJPX7.js → chunk-ITZB7RDX.js} +3 -3
- package/dist/{chunk-5HCDJPX7.js.map → chunk-ITZB7RDX.js.map} +1 -1
- package/dist/chunk-XT5FRTP4.js +7 -0
- package/dist/{chunk-3DIOUFVR.js.map → chunk-XT5FRTP4.js.map} +1 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +43 -9
- package/dist/index.js.map +1 -1
- package/dist/markup/index.d.ts +179 -0
- package/dist/markup/index.js +140 -0
- package/dist/markup/index.js.map +1 -0
- package/package.json +33 -1
- package/dist/ConfirmDialog-R7NZVI2V.js +0 -4
- package/dist/chunk-3DIOUFVR.js +0 -7
- package/dist/chunk-EF5W3EXL.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editorial markup rule — ONE grammar, shared by every product that lets a
|
|
3
|
+
* human type formatted copy into a plain text box.
|
|
4
|
+
*
|
|
5
|
+
* Why this lives in react-os-shell, and why behind its own subpath:
|
|
6
|
+
* the rule is consumed by the admin portal (agent messages + the campaign
|
|
7
|
+
* designer) AND by the public storefront, which has ten dependencies in total
|
|
8
|
+
* and cannot afford the shell's peer graph (a 3D viewer, a PDF renderer, a DXF
|
|
9
|
+
* viewer, an xlsx parser, a Word converter). So this module is deliberately
|
|
10
|
+
* FRAMEWORK-FREE: no React, no JSX, no DOM, no imports at all. `react-os-shell/markup`
|
|
11
|
+
* therefore adds a few hundred bytes of first-party code to a bundle and nothing
|
|
12
|
+
* else — see the subpath's own tsup entry.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IS SHARED IS THE RULE, NOT THE RENDERING. Each product renders the token
|
|
15
|
+
* list its own way, because it must: the web uses CSS classes bound to theme
|
|
16
|
+
* tokens, email must inline every style (mail clients drop classes). Two
|
|
17
|
+
* renderers, one grammar, one writer ({@link applyMark}) — so a toolbar button
|
|
18
|
+
* and a parser can never disagree about what a delimiter means.
|
|
19
|
+
*
|
|
20
|
+
* ## The delimiters
|
|
21
|
+
*
|
|
22
|
+
* Standard markdown, in every product:
|
|
23
|
+
*
|
|
24
|
+
* **phrase** bold
|
|
25
|
+
* _phrase_ italic
|
|
26
|
+
* ~~phrase~~ strikethrough
|
|
27
|
+
* ==phrase== highlight — the brand accent colour, applied to a selection
|
|
28
|
+
*
|
|
29
|
+
* Plus a per-product LEGACY set ({@link STOREFRONT_MARKUP} /
|
|
30
|
+
* {@link CAMPAIGN_MARKUP}) that keeps already-published copy rendering exactly
|
|
31
|
+
* as it does today. Those legacy rules are the only part of this grammar that is
|
|
32
|
+
* meant to be deleted: once stored content has been converted, drop the rule
|
|
33
|
+
* from the product's rule list and nothing else changes.
|
|
34
|
+
*
|
|
35
|
+
* ## Two deliberate delimiter choices
|
|
36
|
+
*
|
|
37
|
+
* 1. ITALIC IS `_phrase_`, NOT `*phrase*`. In the storefront and the campaign
|
|
38
|
+
* designer a single asterisk already means the ACCENT colour, on 704 live
|
|
39
|
+
* instances. Standard markdown says italic. Both cannot be true, so the
|
|
40
|
+
* asterisk keeps its existing meaning and italic takes markdown's OTHER
|
|
41
|
+
* italic marker. Nothing published moves.
|
|
42
|
+
* 2. `_` NEVER FIRES INSIDE A WORD. Without that guard, a mail merge line
|
|
43
|
+
* holding `{{first_name}}` and `{{last_name}}` would italicise everything
|
|
44
|
+
* between the two underscores. The guard is CommonMark's own rule for `_`
|
|
45
|
+
* (an intraword underscore is literal) and it is checked with plain character
|
|
46
|
+
* tests rather than a lookbehind, because a lookbehind is a parse error on
|
|
47
|
+
* Safari below 16.4 and this code ships to a public storefront.
|
|
48
|
+
*/
|
|
49
|
+
/** A style a toolbar button can apply. The first five are inline marks; the
|
|
50
|
+
* last four are the composer-only affordances (a link and the three line
|
|
51
|
+
* prefixes), which only a renderer that draws block content can honour. */
|
|
52
|
+
type MarkStyle = 'bold' | 'italic' | 'strike' | 'code' | 'highlight' | 'link' | 'bullet' | 'number' | 'quote';
|
|
53
|
+
/** What a parsed run MEANS. `accent` is the storefront/campaign brand colour —
|
|
54
|
+
* the legacy `*phrase*` — and `highlight` is its standard-markdown successor;
|
|
55
|
+
* both resolve to the same paint, which is what makes converting stored copy
|
|
56
|
+
* from one to the other invisible. */
|
|
57
|
+
type InlineKind = 'text' | 'bold' | 'italic' | 'strike' | 'code' | 'highlight' | 'accent';
|
|
58
|
+
interface WrapResult {
|
|
59
|
+
text: string;
|
|
60
|
+
/** Where the caret should land — inside the new marks, or after them. */
|
|
61
|
+
selectionStart: number;
|
|
62
|
+
selectionEnd: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Apply a style to `text[start:end]`, returning the new text and where the
|
|
66
|
+
* selection should sit. Toggling is deliberate: pressing Bold on already-bold
|
|
67
|
+
* text unwraps it, so the shortcut behaves like every other editor.
|
|
68
|
+
*
|
|
69
|
+
* Unwrapping only ever recognises the style's OWN fence. An italic button that
|
|
70
|
+
* also unwrapped `*phrase*` would silently strip an accent run in the storefront
|
|
71
|
+
* and the campaign designer, where the asterisk means colour, not slant.
|
|
72
|
+
*/
|
|
73
|
+
declare function applyMark(text: string, start: number, end: number, style: MarkStyle): WrapResult;
|
|
74
|
+
/** One toolbar button. `label` is a text glyph for hosts that draw type
|
|
75
|
+
* (the message composer); hosts with an icon set map `style` to their own.
|
|
76
|
+
* `shortcut` is the bare key the host combines with its platform modifier. */
|
|
77
|
+
interface MarkupTool {
|
|
78
|
+
style: MarkStyle;
|
|
79
|
+
label: string;
|
|
80
|
+
title: string;
|
|
81
|
+
shortcut?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Every button, in the order a writer reaches for them. */
|
|
84
|
+
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
|
+
/** The tools a host shows, resolved to descriptors in {@link MARKUP_TOOLS}
|
|
98
|
+
* order. Keeps a host from hand-rebuilding a descriptor (and drifting on a
|
|
99
|
+
* tooltip) just to pick a subset. */
|
|
100
|
+
declare function markupTools(styles: readonly MarkStyle[]): MarkupTool[];
|
|
101
|
+
/**
|
|
102
|
+
* One delimited run. `open`/`close` are literal strings; a run's inner text may
|
|
103
|
+
* never contain the first character of `close`, which is what keeps this
|
|
104
|
+
* equivalent to the "asterisk, run of non-asterisks, asterisk" regexes the
|
|
105
|
+
* products used before — the same leftmost match, the same single close
|
|
106
|
+
* candidate, so today's stored copy parses exactly as it always has.
|
|
107
|
+
*/
|
|
108
|
+
interface InlineRule {
|
|
109
|
+
readonly open: string;
|
|
110
|
+
readonly close: string;
|
|
111
|
+
readonly kind: Exclude<InlineKind, 'text'>;
|
|
112
|
+
/** CommonMark's `_` rule: neither delimiter may touch a word character, so
|
|
113
|
+
* `snake_case` and `{{first_name}}` stay literal. */
|
|
114
|
+
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;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The standard-markdown marks, shared by every product.
|
|
121
|
+
*
|
|
122
|
+
* `**` and `*` cannot collide even though they share a prefix: a `*` followed
|
|
123
|
+
* immediately by another `*` is an EMPTY accent run, which no rule matches, so
|
|
124
|
+
* at any one position exactly one of the two can fire. Order between them is
|
|
125
|
+
* therefore not observable — which is worth stating, because the obvious
|
|
126
|
+
* assumption ("longest delimiter must be listed first") would make a future
|
|
127
|
+
* reader think this array's order is load-bearing when the empty-run rule is
|
|
128
|
+
* what actually does the work.
|
|
129
|
+
*/
|
|
130
|
+
declare const STANDARD_MARKUP: readonly InlineRule[];
|
|
131
|
+
/**
|
|
132
|
+
* The storefront's rule set: standard markdown plus the ONE legacy run it has
|
|
133
|
+
* published — `*phrase*` in the theme accent. Delete that last entry once
|
|
134
|
+
* stored copy has been converted to `==phrase==`; both kinds already paint the
|
|
135
|
+
* same, so the deletion is invisible.
|
|
136
|
+
*/
|
|
137
|
+
declare const STOREFRONT_MARKUP: readonly InlineRule[];
|
|
138
|
+
/**
|
|
139
|
+
* The campaign designer's rule set: standard markdown plus its three legacy
|
|
140
|
+
* runs — `#phrase#` bold, `*phrase*` accent, `[phrase]` italic. The bold guard
|
|
141
|
+
* is the one the email parser already carried: a `#` followed by a digit does
|
|
142
|
+
* not close a run, so "Ranked #1 and #2" is literal while "Save #20%#" bolds.
|
|
143
|
+
*/
|
|
144
|
+
declare const CAMPAIGN_MARKUP: readonly InlineRule[];
|
|
145
|
+
/** A parsed run. `text` is the inner text with the delimiters removed, so a
|
|
146
|
+
* caller that only wants words ({@link stripInline}) joins the texts and a
|
|
147
|
+
* caller that paints ({@link InlineKind}) switches on the kind. */
|
|
148
|
+
interface InlineToken {
|
|
149
|
+
kind: InlineKind;
|
|
150
|
+
text: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Split authored text into its runs.
|
|
154
|
+
*
|
|
155
|
+
* The returned tokens ALTERNATE, starting and ending with a `text` token that
|
|
156
|
+
* may be empty — exactly the shape `String.split` with one capture group
|
|
157
|
+
* produced in the parsers this replaces. That contract is load-bearing, not
|
|
158
|
+
* cosmetic: the storefront wraps every segment (including the empty ones) in a
|
|
159
|
+
* `<span>`, so preserving the empty tokens is what keeps a published page's
|
|
160
|
+
* markup byte-for-byte identical.
|
|
161
|
+
*
|
|
162
|
+
* A lone delimiter with no partner stays literal — the products' copy contains
|
|
163
|
+
* real stray asterisks, and swallowing one would silently eat a word.
|
|
164
|
+
*/
|
|
165
|
+
declare function tokenizeInline(value: string, rules: readonly InlineRule[]): InlineToken[];
|
|
166
|
+
/**
|
|
167
|
+
* The plain words of an authored string — every paired run unwrapped to its
|
|
168
|
+
* inner text, every unpaired delimiter left alone.
|
|
169
|
+
*
|
|
170
|
+
* This is the ONLY correct way to fill an `alt`, an `aria-label` or a
|
|
171
|
+
* structured-data field from authored copy. Deleting a delimiter character by
|
|
172
|
+
* hand — "replace every asterisk with nothing" — breaks the moment the grammar
|
|
173
|
+
* grows a marker, and it disagrees with the renderer about a stray asterisk,
|
|
174
|
+
* which matters: search engines expect structured data to match the text a
|
|
175
|
+
* visitor actually sees.
|
|
176
|
+
*/
|
|
177
|
+
declare function stripInline(value: string, rules: readonly InlineRule[]): string;
|
|
178
|
+
|
|
179
|
+
export { CAMPAIGN_MARKUP, COPY_FIELD_TOOLS, type InlineKind, type InlineRule, type InlineToken, MARKUP_TOOLS, type MarkStyle, type MarkupTool, STANDARD_MARKUP, STOREFRONT_MARKUP, type WrapResult, applyMark, markupTools, stripInline, tokenizeInline };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/markup/index.ts
|
|
2
|
+
var INLINE = {
|
|
3
|
+
bold: { fence: "**", placeholder: "bold text" },
|
|
4
|
+
italic: { fence: "_", placeholder: "italic text" },
|
|
5
|
+
strike: { fence: "~~", placeholder: "struck out" },
|
|
6
|
+
code: { fence: "`", placeholder: "code" },
|
|
7
|
+
highlight: { fence: "==", placeholder: "highlighted text" }
|
|
8
|
+
};
|
|
9
|
+
var LINE_PREFIX = {
|
|
10
|
+
bullet: () => "- ",
|
|
11
|
+
number: (i) => `${i + 1}. `,
|
|
12
|
+
quote: () => "> "
|
|
13
|
+
};
|
|
14
|
+
function applyMark(text, start, end, style) {
|
|
15
|
+
const selected = text.slice(start, end);
|
|
16
|
+
if (style === "link") {
|
|
17
|
+
const label = selected || "link text";
|
|
18
|
+
const snippet = `[${label}](https://)`;
|
|
19
|
+
return {
|
|
20
|
+
text: text.slice(0, start) + snippet + text.slice(end),
|
|
21
|
+
// Land the caret on the URL — the label is usually already right.
|
|
22
|
+
// Offset spans "[", the label, "](" — three characters plus the label.
|
|
23
|
+
selectionStart: start + label.length + 3,
|
|
24
|
+
selectionEnd: start + snippet.length - 1
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const prefix = LINE_PREFIX[style];
|
|
28
|
+
if (prefix) {
|
|
29
|
+
const from = text.lastIndexOf("\n", Math.max(start - 1, 0)) + 1;
|
|
30
|
+
const toIdx = text.indexOf("\n", end);
|
|
31
|
+
const to = toIdx === -1 ? text.length : toIdx;
|
|
32
|
+
const block = text.slice(from, to);
|
|
33
|
+
const lines = block.split("\n");
|
|
34
|
+
const marked = lines.every((l, i) => l.startsWith(prefix(i)));
|
|
35
|
+
const next = lines.map((l, i) => marked ? l.slice(prefix(i).length) : prefix(i) + l).join("\n");
|
|
36
|
+
return {
|
|
37
|
+
text: text.slice(0, from) + next + text.slice(to),
|
|
38
|
+
selectionStart: from,
|
|
39
|
+
selectionEnd: from + next.length
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const { fence, placeholder } = INLINE[style];
|
|
43
|
+
const before = text.slice(Math.max(0, start - fence.length), start);
|
|
44
|
+
const after = text.slice(end, end + fence.length);
|
|
45
|
+
if (before === fence && after === fence) {
|
|
46
|
+
return {
|
|
47
|
+
text: text.slice(0, start - fence.length) + selected + text.slice(end + fence.length),
|
|
48
|
+
selectionStart: start - fence.length,
|
|
49
|
+
selectionEnd: start - fence.length + selected.length
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const inner = selected || placeholder;
|
|
53
|
+
return {
|
|
54
|
+
text: text.slice(0, start) + fence + inner + fence + text.slice(end),
|
|
55
|
+
selectionStart: start + fence.length,
|
|
56
|
+
selectionEnd: start + fence.length + inner.length
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var MARKUP_TOOLS = [
|
|
60
|
+
{ style: "bold", label: "B", title: "Bold (\u2318B)", shortcut: "b" },
|
|
61
|
+
{ style: "italic", label: "I", title: "Italic (\u2318I)", shortcut: "i" },
|
|
62
|
+
{ style: "strike", label: "S", title: "Strikethrough" },
|
|
63
|
+
{ style: "highlight", label: "\u25C6", title: "Highlight in the brand colour" },
|
|
64
|
+
{ style: "code", label: "\u2039\u203A", title: "Code" },
|
|
65
|
+
{ style: "link", label: "\u{1F517}", title: "Link (\u2318K)", shortcut: "k" },
|
|
66
|
+
{ style: "bullet", label: "\u2022", title: "Bulleted list" },
|
|
67
|
+
{ style: "number", label: "1.", title: "Numbered list" },
|
|
68
|
+
{ style: "quote", label: "\u275D", title: "Quote" }
|
|
69
|
+
];
|
|
70
|
+
var COPY_FIELD_TOOLS = [
|
|
71
|
+
"bold",
|
|
72
|
+
"italic",
|
|
73
|
+
"strike",
|
|
74
|
+
"highlight"
|
|
75
|
+
];
|
|
76
|
+
function markupTools(styles) {
|
|
77
|
+
return MARKUP_TOOLS.filter((t) => styles.includes(t.style));
|
|
78
|
+
}
|
|
79
|
+
var STANDARD_MARKUP = [
|
|
80
|
+
{ open: "**", close: "**", kind: "bold" },
|
|
81
|
+
{ open: "==", close: "==", kind: "highlight" },
|
|
82
|
+
{ open: "~~", close: "~~", kind: "strike" },
|
|
83
|
+
{ open: "_", close: "_", kind: "italic", wordBoundary: true }
|
|
84
|
+
];
|
|
85
|
+
var STOREFRONT_MARKUP = [
|
|
86
|
+
...STANDARD_MARKUP,
|
|
87
|
+
{ open: "*", close: "*", kind: "accent" }
|
|
88
|
+
];
|
|
89
|
+
var CAMPAIGN_MARKUP = [
|
|
90
|
+
...STANDARD_MARKUP,
|
|
91
|
+
{ open: "*", close: "*", kind: "accent" },
|
|
92
|
+
{ open: "#", close: "#", kind: "bold", notBeforeDigit: true },
|
|
93
|
+
{ open: "[", close: "]", kind: "italic" }
|
|
94
|
+
];
|
|
95
|
+
function wordish(ch) {
|
|
96
|
+
return !/\s/.test(ch) && !/[!-\/:-@[-`{-~]/.test(ch);
|
|
97
|
+
}
|
|
98
|
+
function tokenizeInline(value, rules) {
|
|
99
|
+
const out = [];
|
|
100
|
+
let buf = "";
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < value.length) {
|
|
103
|
+
const hit = matchAt(value, i, rules);
|
|
104
|
+
if (hit) {
|
|
105
|
+
out.push({ kind: "text", text: buf });
|
|
106
|
+
buf = "";
|
|
107
|
+
out.push({ kind: hit.rule.kind, text: hit.inner });
|
|
108
|
+
i = hit.end;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
buf += value[i];
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
out.push({ kind: "text", text: buf });
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function matchAt(value, i, rules) {
|
|
118
|
+
for (const rule of rules) {
|
|
119
|
+
if (!value.startsWith(rule.open, i)) continue;
|
|
120
|
+
if (rule.wordBoundary && i > 0 && wordish(value[i - 1])) continue;
|
|
121
|
+
const from = i + rule.open.length;
|
|
122
|
+
const closeAt = value.indexOf(rule.close, from);
|
|
123
|
+
if (closeAt < from + 1) continue;
|
|
124
|
+
const inner = value.slice(from, closeAt);
|
|
125
|
+
if (inner.includes(rule.close[0])) continue;
|
|
126
|
+
const end = closeAt + rule.close.length;
|
|
127
|
+
const next = value[end];
|
|
128
|
+
if (rule.notBeforeDigit && next !== void 0 && next >= "0" && next <= "9") continue;
|
|
129
|
+
if (rule.wordBoundary && next !== void 0 && wordish(next)) continue;
|
|
130
|
+
return { rule, inner, end };
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function stripInline(value, rules) {
|
|
135
|
+
return tokenizeInline(value, rules).map((t) => t.text).join("");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { CAMPAIGN_MARKUP, COPY_FIELD_TOOLS, MARKUP_TOOLS, STANDARD_MARKUP, STOREFRONT_MARKUP, applyMark, markupTools, stripInline, tokenizeInline };
|
|
139
|
+
//# sourceMappingURL=index.js.map
|
|
140
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-os-shell",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"types": "./dist/apps/index.d.ts",
|
|
29
29
|
"import": "./dist/apps/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./markup": {
|
|
32
|
+
"types": "./dist/markup/index.d.ts",
|
|
33
|
+
"import": "./dist/markup/index.js"
|
|
34
|
+
},
|
|
31
35
|
"./styles.css": "./dist/styles.css",
|
|
32
36
|
"./themes.css": "./dist/themes.css"
|
|
33
37
|
},
|
|
@@ -51,10 +55,14 @@
|
|
|
51
55
|
"xlsx": "*"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
58
|
+
"@headlessui/react": "^2.2.10",
|
|
59
|
+
"@heroicons/react": "^2.2.0",
|
|
60
|
+
"@tanstack/react-query": "^5.100.5",
|
|
54
61
|
"@types/jsdom": "^28.0.3",
|
|
55
62
|
"@types/node": "^22.0.0",
|
|
56
63
|
"@types/react": "^18.2.0",
|
|
57
64
|
"@types/react-dom": "^18.2.0",
|
|
65
|
+
"axios": "^1.15.2",
|
|
58
66
|
"dxf-viewer": "^1.0.47",
|
|
59
67
|
"esbuild": "^0.27.0",
|
|
60
68
|
"jsdom": "^26.1.0",
|
|
@@ -64,6 +72,9 @@
|
|
|
64
72
|
"playwright": "^1.48.0",
|
|
65
73
|
"react": "^18.2.0",
|
|
66
74
|
"react-dom": "^18.2.0",
|
|
75
|
+
"react-hook-form": "^7.74.0",
|
|
76
|
+
"react-router-dom": "^7.14.2",
|
|
77
|
+
"tailwindcss": "^4.2.4",
|
|
67
78
|
"tsup": "^8.0.0",
|
|
68
79
|
"typescript": "^5.3.0",
|
|
69
80
|
"xlsx": "^0.18.5"
|
|
@@ -89,6 +100,27 @@
|
|
|
89
100
|
"node": ">=20"
|
|
90
101
|
},
|
|
91
102
|
"peerDependenciesMeta": {
|
|
103
|
+
"@headlessui/react": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@heroicons/react": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@tanstack/react-query": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"axios": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"react-hook-form": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"react-router-dom": {
|
|
119
|
+
"optional": true
|
|
120
|
+
},
|
|
121
|
+
"tailwindcss": {
|
|
122
|
+
"optional": true
|
|
123
|
+
},
|
|
92
124
|
"xlsx": {
|
|
93
125
|
"optional": true
|
|
94
126
|
},
|