@citisen/litearea 0.1.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +514 -0
  3. package/README.zh.md +362 -0
  4. package/dist/grammars.cjs +1228 -0
  5. package/dist/grammars.cjs.map +1 -0
  6. package/dist/grammars.js +1213 -0
  7. package/dist/grammars.js.map +1 -0
  8. package/dist/index.cjs +3103 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.js +3040 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/react.cjs +3032 -0
  13. package/dist/react.cjs.map +1 -0
  14. package/dist/react.js +3010 -0
  15. package/dist/react.js.map +1 -0
  16. package/dist/styles.cjs +453 -0
  17. package/dist/styles.cjs.map +1 -0
  18. package/dist/styles.css +432 -0
  19. package/dist/styles.js +447 -0
  20. package/dist/styles.js.map +1 -0
  21. package/dist/types/core/complete.d.ts +70 -0
  22. package/dist/types/core/complete.d.ts.map +1 -0
  23. package/dist/types/core/format.d.ts +34 -0
  24. package/dist/types/core/format.d.ts.map +1 -0
  25. package/dist/types/core/grammar.d.ts +14 -0
  26. package/dist/types/core/grammar.d.ts.map +1 -0
  27. package/dist/types/core/hover.d.ts +23 -0
  28. package/dist/types/core/hover.d.ts.map +1 -0
  29. package/dist/types/core/index.d.ts +12 -0
  30. package/dist/types/core/index.d.ts.map +1 -0
  31. package/dist/types/core/inspect.d.ts +34 -0
  32. package/dist/types/core/inspect.d.ts.map +1 -0
  33. package/dist/types/core/rank.d.ts +82 -0
  34. package/dist/types/core/rank.d.ts.map +1 -0
  35. package/dist/types/core/scan.d.ts +51 -0
  36. package/dist/types/core/scan.d.ts.map +1 -0
  37. package/dist/types/core/segments.d.ts +44 -0
  38. package/dist/types/core/segments.d.ts.map +1 -0
  39. package/dist/types/core/text.d.ts +113 -0
  40. package/dist/types/core/text.d.ts.map +1 -0
  41. package/dist/types/core/types.d.ts +604 -0
  42. package/dist/types/core/types.d.ts.map +1 -0
  43. package/dist/types/core/vocabulary.d.ts +82 -0
  44. package/dist/types/core/vocabulary.d.ts.map +1 -0
  45. package/dist/types/dom/create.d.ts +17 -0
  46. package/dist/types/dom/create.d.ts.map +1 -0
  47. package/dist/types/dom/editing.d.ts +89 -0
  48. package/dist/types/dom/editing.d.ts.map +1 -0
  49. package/dist/types/dom/editor.d.ts +366 -0
  50. package/dist/types/dom/editor.d.ts.map +1 -0
  51. package/dist/types/dom/index.d.ts +9 -0
  52. package/dist/types/dom/index.d.ts.map +1 -0
  53. package/dist/types/dom/mirror.d.ts +107 -0
  54. package/dist/types/dom/mirror.d.ts.map +1 -0
  55. package/dist/types/dom/overlay.d.ts +52 -0
  56. package/dist/types/dom/overlay.d.ts.map +1 -0
  57. package/dist/types/dom/popup.d.ts +95 -0
  58. package/dist/types/dom/popup.d.ts.map +1 -0
  59. package/dist/types/dom/support.d.ts +41 -0
  60. package/dist/types/dom/support.d.ts.map +1 -0
  61. package/dist/types/dom/tooltip.d.ts +39 -0
  62. package/dist/types/dom/tooltip.d.ts.map +1 -0
  63. package/dist/types/grammars/dshFont.d.ts +127 -0
  64. package/dist/types/grammars/dshFont.d.ts.map +1 -0
  65. package/dist/types/grammars/dshSentry.d.ts +84 -0
  66. package/dist/types/grammars/dshSentry.d.ts.map +1 -0
  67. package/dist/types/grammars/index.d.ts +3 -0
  68. package/dist/types/grammars/index.d.ts.map +1 -0
  69. package/dist/types/index.d.ts +15 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/react/index.d.ts +91 -0
  72. package/dist/types/react/index.d.ts.map +1 -0
  73. package/dist/types/styles.d.ts +29 -0
  74. package/dist/types/styles.d.ts.map +1 -0
  75. package/docs/architecture.md +316 -0
  76. package/docs/completion.md +320 -0
  77. package/docs/grammar.md +823 -0
  78. package/package.json +105 -0
  79. package/scripts/browser-check.mjs +838 -0
  80. package/scripts/build-css.mjs +35 -0
  81. package/scripts/release.mjs +91 -0
  82. package/scripts/verify-package.mjs +253 -0
  83. package/src/core/complete.ts +286 -0
  84. package/src/core/format.ts +71 -0
  85. package/src/core/grammar.ts +40 -0
  86. package/src/core/hover.ts +129 -0
  87. package/src/core/index.ts +98 -0
  88. package/src/core/inspect.ts +198 -0
  89. package/src/core/rank.ts +317 -0
  90. package/src/core/scan.ts +720 -0
  91. package/src/core/segments.ts +185 -0
  92. package/src/core/text.ts +238 -0
  93. package/src/core/types.ts +681 -0
  94. package/src/core/vocabulary.ts +196 -0
  95. package/src/dom/create.ts +31 -0
  96. package/src/dom/editing.ts +213 -0
  97. package/src/dom/editor.ts +1143 -0
  98. package/src/dom/index.ts +46 -0
  99. package/src/dom/mirror.ts +305 -0
  100. package/src/dom/overlay.ts +106 -0
  101. package/src/dom/popup.ts +323 -0
  102. package/src/dom/support.ts +88 -0
  103. package/src/dom/tooltip.ts +112 -0
  104. package/src/grammars/dshFont.ts +1004 -0
  105. package/src/grammars/dshSentry.ts +742 -0
  106. package/src/grammars/index.ts +57 -0
  107. package/src/index.ts +122 -0
  108. package/src/react/index.tsx +248 -0
  109. package/src/styles.ts +529 -0
@@ -0,0 +1,57 @@
1
+ // ─── reference grammars ─────────────────────────────────────────────────────
2
+ //
3
+ // The core of litearea ships NO syntax. There is no built-in language, no
4
+ // language identifier to switch on, and nothing in `src/core/` that knows what a
5
+ // font stack or a favicon state is. That is the whole claim of the library, and
6
+ // these two files are how it is checked rather than merely asserted.
7
+ //
8
+ // They are two real DSLs, taken from two real plugins, expressed entirely as data
9
+ // the engine is handed:
10
+ //
11
+ // dshFontQueryGrammar() a CSS font-family list with a weight written beside
12
+ // the family it belongs to
13
+ // dshSentryStyleGrammar() a line-oriented appearance document, one line per
14
+ // session state
15
+ //
16
+ // Between them they exercise every part of the contract: regex rules and
17
+ // vocabulary rules, multi-word phrases, a lookahead, a scope FUNCTION that reads
18
+ // the analysis, a dynamic vocabulary resolved from the host machine, all four
19
+ // diagnostic sources, completion with both insertion modes, hover, and semantic
20
+ // decorations. If a future change to the engine broke any of that, these two files
21
+ // would stop working — which is why they are in the test suite and why the
22
+ // documentation points at them as the worked examples.
23
+ //
24
+ // They are importable because a host should not have to retype a language that
25
+ // already exists. They are NOT built in: pass one to `createEditor` or import
26
+ // nothing from this module and write your own.
27
+
28
+ export {
29
+ DSH_FONT_INFO_CODES,
30
+ FONT_COMMON_FAMILIES,
31
+ FONT_GENERIC_FAMILIES,
32
+ FONT_WEIGHT_LABELS,
33
+ FONT_WEIGHT_SCALE,
34
+ FONT_WEIGHT_WORDS,
35
+ dshFontQueryGrammar,
36
+ fontFaceWeights,
37
+ fontWeightWord,
38
+ quoteFontFamily,
39
+ type DshFontEntry,
40
+ type DshFontProblem,
41
+ type DshFontQueryOptions,
42
+ type DshFontState,
43
+ } from './dshFont.js'
44
+
45
+ export {
46
+ DSH_SENTRY_COLORS,
47
+ DSH_SENTRY_STATES,
48
+ DSH_SENTRY_VOCABULARY,
49
+ dshSentryStyleGrammar,
50
+ type DshSentryLine,
51
+ type DshSentryLook,
52
+ type DshSentryProblem,
53
+ type DshSentrySlot,
54
+ type DshSentryState,
55
+ type DshSentryStyleOptions,
56
+ type DshSentryWord,
57
+ } from './dshSentry.js'
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ // ─── litearea ───────────────────────────────────────────────────────────────
2
+ //
3
+ // A textarea-based code editor: undo that works, syntax highlighting from rules you
4
+ // write, VSCode-shaped completion, diagnostics, hover, and a box that fits its
5
+ // content. No CodeMirror, no Monaco, no runtime dependency.
6
+ //
7
+ // The package has three entry points and this is the largest:
8
+ //
9
+ // @citisen/litearea the engine and the DOM layer (this file)
10
+ // @citisen/litearea/react a React binding over the same editor
11
+ // @citisen/litearea/grammars two worked grammars for two real DSLs
12
+ //
13
+ // Nothing here knows any syntax. `src/core/` is pure and needs no DOM; `src/dom/`
14
+ // needs a document and no framework.
15
+
16
+ export type {
17
+ CheckRule,
18
+ Completion,
19
+ CompletionContext,
20
+ CompletionRow,
21
+ CompletionSource,
22
+ CompletionTrigger,
23
+ Decoration,
24
+ DiagnoseContext,
25
+ Diagnostic,
26
+ Grammar,
27
+ HoverContext,
28
+ HoverInfo,
29
+ LineInfo,
30
+ MatchRule,
31
+ Range,
32
+ RegionRule,
33
+ ResolvedVocabulary,
34
+ Rule,
35
+ RuleContext,
36
+ RuleMatch,
37
+ Scope,
38
+ ScopeSpec,
39
+ Severity,
40
+ SuggestionItem,
41
+ Token,
42
+ VocabularyContext,
43
+ VocabularyEntry,
44
+ WordInfo,
45
+ WordsRule,
46
+ WordsSource,
47
+ } from './core/types.js'
48
+
49
+ export type { PaintSegment, SegmentInput } from './core/segments.js'
50
+
51
+ export { excerpt, fillTemplate, listPhrase } from './core/format.js'
52
+
53
+ export {
54
+ clamp,
55
+ containsOffset,
56
+ isEmptyRange,
57
+ isOffset,
58
+ isWordChar,
59
+ lineAt,
60
+ lineIndexAt,
61
+ lineStarts,
62
+ scopeAt,
63
+ tokenAfter,
64
+ tokenAt,
65
+ tokenBefore,
66
+ tokensOnLine,
67
+ wordInfoAt,
68
+ } from './core/text.js'
69
+
70
+ export {
71
+ asResolvedVocabulary,
72
+ defineVocabulary,
73
+ resolveWordsSource,
74
+ vocabularyWords,
75
+ type VocabularySpec,
76
+ } from './core/vocabulary.js'
77
+
78
+ export {
79
+ isResolvedGrammar,
80
+ resolveGrammar,
81
+ scan,
82
+ type ResolvedGrammar,
83
+ type ScanResult,
84
+ } from './core/scan.js'
85
+
86
+ export { inspect, normalizeDiagnostics, type Inspection } from './core/inspect.js'
87
+
88
+ export {
89
+ buildSegments,
90
+ segmentClasses,
91
+ } from './core/segments.js'
92
+
93
+ export {
94
+ fuzzyMatch,
95
+ highlightSegments,
96
+ isWordStart,
97
+ rank,
98
+ type FuzzyMatch,
99
+ type Ranked,
100
+ type RankOptions,
101
+ } from './core/rank.js'
102
+
103
+ export {
104
+ applyCompletion,
105
+ complete,
106
+ type AppliedCompletion,
107
+ type CompletionRequest,
108
+ } from './core/complete.js'
109
+
110
+ export { diagnosticHover, resolveHover } from './core/hover.js'
111
+
112
+ export { defineCompletion, defineGrammar } from './core/grammar.js'
113
+
114
+ export {
115
+ LITEAREA_STYLES,
116
+ decorationClass,
117
+ injectStyles,
118
+ scopeClass,
119
+ severityClass,
120
+ } from './styles.js'
121
+
122
+ export * from './dom/index.js'
@@ -0,0 +1,248 @@
1
+ // ─── React binding ──────────────────────────────────────────────────────────
2
+ //
3
+ // This is a thin wrapper, and what makes it thin is a decision a reader should
4
+ // understand before changing it: **the component never passes the text through
5
+ // React.**
6
+ //
7
+ // The obvious React wrapper takes `value` and `onChange` and re-renders the
8
+ // textarea on every keystroke. That wrapper cannot work. Setting a textarea's value
9
+ // from script replaces its content, which discards the browser's undo stack and
10
+ // resets the caret to the end — so the two most common complaints about the editors
11
+ // this library replaces ("undo is gone", "the caret jumps") are not two bugs but
12
+ // one, and it is caused by exactly that pattern. The text belongs to the DOM node;
13
+ // React's job here is to create the node once and then leave it alone.
14
+ //
15
+ // So this component renders an empty div, mounts the imperative editor inside it,
16
+ // and keeps a ref to the latest props so the editor's callbacks are never stale. The
17
+ // grammar is handed over through a live proxy, which lets a host rebuild its grammar
18
+ // on every render without forcing the editor to be rebuilt: the editor re-resolves
19
+ // the rules on `refresh()`, and the undo history survives a language change.
20
+
21
+ import * as React from 'react'
22
+ import type { Completion, Diagnostic, Grammar, HoverInfo } from '../core/types.js'
23
+ import type { ResolvedGrammar } from '../core/scan.js'
24
+ import { LiteArea, type LiteAreaCompletion, type LiteAreaHover, type LiteAreaSizing } from '../dom/editor.js'
25
+ import type { TextSelection } from '../dom/editing.js'
26
+
27
+ /** Everything the React component accepts. */
28
+ export interface LiteAreaEditorProps<State = unknown> {
29
+ /**
30
+ * The language.
31
+ *
32
+ * It does NOT have to be stable: the component hands the engine a live view of
33
+ * whatever was passed last, so rebuilding the object on every render is safe and
34
+ * the editor is not rebuilt.
35
+ */
36
+ grammar: Grammar<State> | ResolvedGrammar<State>
37
+ /**
38
+ * The text to start with. Read ONCE, when the editor mounts.
39
+ *
40
+ * This is the prop to use. A changing initial value is what `value` is for, and it
41
+ * cannot be done by re-rendering, because that is the thing that breaks undo.
42
+ */
43
+ defaultValue?: string
44
+ /**
45
+ * Text the host wants shown.
46
+ *
47
+ * When this differs from the field it is written — through the browser's editing
48
+ * pipeline, so it stays undoable. Leaving it undefined is the normal case: the
49
+ * field owns the text.
50
+ */
51
+ value?: string
52
+ /** Whether a `value` write should be one undoable edit. Default `false`. */
53
+ preserveHistory?: boolean
54
+ /** Whether the user may edit. */
55
+ readOnly?: boolean
56
+ /** Shown while the box is empty. */
57
+ placeholder?: string
58
+ /** Whether the browser may spell-check. Default `false`. */
59
+ spellCheck?: boolean
60
+ /** The accessible name. */
61
+ ariaLabel?: string
62
+ /** A class on the wrapper element. */
63
+ className?: string
64
+ /** Styles for the wrapper element. */
65
+ style?: React.CSSProperties
66
+ /**
67
+ * CSS custom properties for this instance — a theme, a font, or both.
68
+ *
69
+ * Unlike the options read at mount, this one is applied live, and an inline object literal is
70
+ * fine: the records are compared by value, so a fresh `{ font: '…' }` on every render does not
71
+ * mean work on every render.
72
+ */
73
+ variables?: Record<string, string>
74
+ /** How the box follows its content. Read when the editor mounts. */
75
+ sizing?: LiteAreaSizing
76
+ /** How the list behaves, or `false` to switch completions off. Read when the editor mounts. */
77
+ completion?: LiteAreaCompletion | false
78
+ /** How tooltips behave, or `false` to switch them off. Read when the editor mounts. */
79
+ hover?: LiteAreaHover | false
80
+ /** Whether semantic decorations are painted. Read when the editor mounts. */
81
+ decorations?: boolean
82
+ /** Whether to inject the stylesheet. Read when the editor mounts. */
83
+ injectStyles?: boolean
84
+ /** A CSP nonce for the injected stylesheet. */
85
+ styleNonce?: string
86
+ /** Called after a user edit, with the new text. */
87
+ onChange?: (value: string) => void
88
+ /** Called when the caret or selection moves. */
89
+ onSelectionChange?: (selection: TextSelection) => void
90
+ /** Called when the problem list changes. */
91
+ onDiagnostics?: (diagnostics: readonly Diagnostic[]) => void
92
+ /** Called when the list opens, filters, or closes. */
93
+ onCompletion?: (completion: Completion | undefined) => void
94
+ /** Called when a tooltip appears or goes away. */
95
+ onHover?: (info: HoverInfo | undefined) => void
96
+ /**
97
+ * Handed the editor once it exists, and `undefined` when it goes away.
98
+ *
99
+ * The escape hatch for anything the props do not cover: undo and redo, focusing
100
+ * the field, or reading the diagnostics directly.
101
+ */
102
+ editorRef?: (editor: LiteArea<State> | undefined) => void
103
+ }
104
+
105
+ /** The props as the editor wants them, without the React-only ones. */
106
+ interface EditorCoreProps {
107
+ placeholder?: string
108
+ spellCheck?: boolean
109
+ ariaLabel?: string
110
+ sizing?: LiteAreaSizing
111
+ completion?: LiteAreaCompletion | false
112
+ hover?: LiteAreaHover | false
113
+ decorations?: boolean
114
+ injectStyles?: boolean
115
+ styleNonce?: string
116
+ }
117
+
118
+ /**
119
+ * A view of an object that always reads the newest value.
120
+ *
121
+ * Its purpose is narrower than a general proxy: the engine takes the grammar object
122
+ * once and reads its rules from it, so a host that writes `grammar={{...}}` inline
123
+ * would hand over a fresh object on every render. Passing a live view instead means
124
+ * the object's identity never changes, so nothing is rebuilt, while `refresh()` still
125
+ * sees the newest rules.
126
+ * @param holder - the box holding the current value.
127
+ * @returns the live view.
128
+ */
129
+ function liveGrammar<State>(holder: { current: Grammar<State> | ResolvedGrammar<State> }): Grammar<State> | ResolvedGrammar<State> {
130
+ return new Proxy({} as Grammar<State>, {
131
+ get: (_target, key) => Reflect.get(holder.current as object, key),
132
+ has: (_target, key) => Reflect.has(holder.current as object, key),
133
+ ownKeys: () => Reflect.ownKeys(holder.current as object),
134
+ getOwnPropertyDescriptor: (_target, key) =>
135
+ Reflect.getOwnPropertyDescriptor(holder.current as object, key),
136
+ }) as Grammar<State>
137
+ }
138
+
139
+ /**
140
+ * Whether two variable records say the same thing.
141
+ * @param left - one record, or undefined.
142
+ * @param right - the other.
143
+ * @returns whether every key and value matches.
144
+ */
145
+ function sameVariables(
146
+ left: Record<string, string> | undefined,
147
+ right: Record<string, string> | undefined,
148
+ ): boolean {
149
+ if (left === right) return true
150
+ const a = Object.entries(left ?? {})
151
+ const b = Object.entries(right ?? {})
152
+ if (a.length !== b.length) return false
153
+ return a.every(([key, value]) => (right ?? {})[key] === value)
154
+ }
155
+
156
+ /**
157
+ * A code editor, as a React component.
158
+ *
159
+ * See the module comment for why this does not take the text through React state.
160
+ * @param props - the grammar and the behaviour.
161
+ * @returns the wrapper element the editor mounts into.
162
+ */
163
+ export function LiteAreaEditor<State = unknown>(
164
+ props: LiteAreaEditorProps<State>,
165
+ ): React.ReactElement {
166
+ const hostRef = React.useRef<HTMLDivElement | null>(null)
167
+ const editorRef = React.useRef<LiteArea<State> | undefined>(undefined)
168
+ /** The newest props, so the editor's callbacks are never a render behind. */
169
+ const latest = React.useRef(props)
170
+ latest.current = props
171
+ /** The grammar box every live view reads through. */
172
+ const grammarBox = React.useRef(props.grammar)
173
+ grammarBox.current = props.grammar
174
+ const live = React.useMemo(() => liveGrammar<State>(grammarBox), [])
175
+ /** The variable record last applied, so an unchanged one is not applied again. */
176
+ const variablesRef = React.useRef<Record<string, string> | undefined>(undefined)
177
+
178
+ React.useLayoutEffect(() => {
179
+ const host = hostRef.current
180
+ if (host === null) return
181
+ const core: EditorCoreProps = latest.current
182
+ const editor = new LiteArea<State>({
183
+ grammar: live,
184
+ value: latest.current.defaultValue ?? latest.current.value ?? '',
185
+ readOnly: latest.current.readOnly === true,
186
+ ...core,
187
+ // Every callback reads through `latest`, so a host that passes a new closure
188
+ // on each render does not need the editor to know about it.
189
+ onChange: (value) => {
190
+ latest.current.onChange?.(value)
191
+ },
192
+ onSelectionChange: (selection) => {
193
+ latest.current.onSelectionChange?.(selection)
194
+ },
195
+ onDiagnostics: (diagnostics) => {
196
+ latest.current.onDiagnostics?.(diagnostics)
197
+ },
198
+ onCompletion: (completion) => {
199
+ latest.current.onCompletion?.(completion)
200
+ },
201
+ onHover: (info) => {
202
+ latest.current.onHover?.(info)
203
+ },
204
+ })
205
+ host.appendChild(editor.element)
206
+ editorRef.current = editor
207
+ latest.current.editorRef?.(editor)
208
+ return () => {
209
+ editor.destroy()
210
+ editorRef.current = undefined
211
+ latest.current.editorRef?.(undefined)
212
+ }
213
+ // Mounted once, on purpose. Re-creating the editor would discard the DOM node the
214
+ // browser's undo history belongs to, which is the one thing this library promises
215
+ // not to do.
216
+ }, [live])
217
+
218
+ React.useLayoutEffect(() => {
219
+ const editor = editorRef.current
220
+ if (editor === undefined) return
221
+ // The variable records are compared by VALUE, because a host writing
222
+ // `variables={{ font: '…' }}` inline produces a fresh object on every render, and a
223
+ // reference check would re-measure the box on every render for nothing.
224
+ if (!sameVariables(props.variables, variablesRef.current)) {
225
+ variablesRef.current = props.variables
226
+ editor.setVariables(props.variables ?? {})
227
+ }
228
+ // The grammar may have been rebuilt this render, and the editor resolved its
229
+ // rules when it last looked. Re-resolving is cheap and is what keeps a live
230
+ // grammar honest.
231
+ editor.refresh()
232
+ // A `value` the field does not already have is a host asking for a write. Writing
233
+ // it through the pipeline keeps it undoable; assigning it would not.
234
+ if (props.value !== undefined && editor.value !== props.value) {
235
+ editor.setValue(props.value, props.preserveHistory === true)
236
+ }
237
+ if (editor.input.readOnly !== (props.readOnly === true)) {
238
+ editor.input.readOnly = props.readOnly === true
239
+ editor.element.classList.toggle('litearea-readonly', props.readOnly === true)
240
+ }
241
+ })
242
+
243
+ return React.createElement('div', {
244
+ ref: hostRef,
245
+ className: props.className,
246
+ style: props.style,
247
+ })
248
+ }