@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.
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/README.zh.md +362 -0
- package/dist/grammars.cjs +1228 -0
- package/dist/grammars.cjs.map +1 -0
- package/dist/grammars.js +1213 -0
- package/dist/grammars.js.map +1 -0
- package/dist/index.cjs +3103 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +3040 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +3032 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.js +3010 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.cjs +453 -0
- package/dist/styles.cjs.map +1 -0
- package/dist/styles.css +432 -0
- package/dist/styles.js +447 -0
- package/dist/styles.js.map +1 -0
- package/dist/types/core/complete.d.ts +70 -0
- package/dist/types/core/complete.d.ts.map +1 -0
- package/dist/types/core/format.d.ts +34 -0
- package/dist/types/core/format.d.ts.map +1 -0
- package/dist/types/core/grammar.d.ts +14 -0
- package/dist/types/core/grammar.d.ts.map +1 -0
- package/dist/types/core/hover.d.ts +23 -0
- package/dist/types/core/hover.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +12 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/inspect.d.ts +34 -0
- package/dist/types/core/inspect.d.ts.map +1 -0
- package/dist/types/core/rank.d.ts +82 -0
- package/dist/types/core/rank.d.ts.map +1 -0
- package/dist/types/core/scan.d.ts +51 -0
- package/dist/types/core/scan.d.ts.map +1 -0
- package/dist/types/core/segments.d.ts +44 -0
- package/dist/types/core/segments.d.ts.map +1 -0
- package/dist/types/core/text.d.ts +113 -0
- package/dist/types/core/text.d.ts.map +1 -0
- package/dist/types/core/types.d.ts +604 -0
- package/dist/types/core/types.d.ts.map +1 -0
- package/dist/types/core/vocabulary.d.ts +82 -0
- package/dist/types/core/vocabulary.d.ts.map +1 -0
- package/dist/types/dom/create.d.ts +17 -0
- package/dist/types/dom/create.d.ts.map +1 -0
- package/dist/types/dom/editing.d.ts +89 -0
- package/dist/types/dom/editing.d.ts.map +1 -0
- package/dist/types/dom/editor.d.ts +366 -0
- package/dist/types/dom/editor.d.ts.map +1 -0
- package/dist/types/dom/index.d.ts +9 -0
- package/dist/types/dom/index.d.ts.map +1 -0
- package/dist/types/dom/mirror.d.ts +107 -0
- package/dist/types/dom/mirror.d.ts.map +1 -0
- package/dist/types/dom/overlay.d.ts +52 -0
- package/dist/types/dom/overlay.d.ts.map +1 -0
- package/dist/types/dom/popup.d.ts +95 -0
- package/dist/types/dom/popup.d.ts.map +1 -0
- package/dist/types/dom/support.d.ts +41 -0
- package/dist/types/dom/support.d.ts.map +1 -0
- package/dist/types/dom/tooltip.d.ts +39 -0
- package/dist/types/dom/tooltip.d.ts.map +1 -0
- package/dist/types/grammars/dshFont.d.ts +127 -0
- package/dist/types/grammars/dshFont.d.ts.map +1 -0
- package/dist/types/grammars/dshSentry.d.ts +84 -0
- package/dist/types/grammars/dshSentry.d.ts.map +1 -0
- package/dist/types/grammars/index.d.ts +3 -0
- package/dist/types/grammars/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/react/index.d.ts +91 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/styles.d.ts +29 -0
- package/dist/types/styles.d.ts.map +1 -0
- package/docs/architecture.md +316 -0
- package/docs/completion.md +320 -0
- package/docs/grammar.md +823 -0
- package/package.json +105 -0
- package/scripts/browser-check.mjs +838 -0
- package/scripts/build-css.mjs +35 -0
- package/scripts/release.mjs +91 -0
- package/scripts/verify-package.mjs +253 -0
- package/src/core/complete.ts +286 -0
- package/src/core/format.ts +71 -0
- package/src/core/grammar.ts +40 -0
- package/src/core/hover.ts +129 -0
- package/src/core/index.ts +98 -0
- package/src/core/inspect.ts +198 -0
- package/src/core/rank.ts +317 -0
- package/src/core/scan.ts +720 -0
- package/src/core/segments.ts +185 -0
- package/src/core/text.ts +238 -0
- package/src/core/types.ts +681 -0
- package/src/core/vocabulary.ts +196 -0
- package/src/dom/create.ts +31 -0
- package/src/dom/editing.ts +213 -0
- package/src/dom/editor.ts +1143 -0
- package/src/dom/index.ts +46 -0
- package/src/dom/mirror.ts +305 -0
- package/src/dom/overlay.ts +106 -0
- package/src/dom/popup.ts +323 -0
- package/src/dom/support.ts +88 -0
- package/src/dom/tooltip.ts +112 -0
- package/src/grammars/dshFont.ts +1004 -0
- package/src/grammars/dshSentry.ts +742 -0
- package/src/grammars/index.ts +57 -0
- package/src/index.ts +122 -0
- package/src/react/index.tsx +248 -0
- 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
|
+
}
|