@singapore-editor/gutters 0.1.1

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 ADDED
@@ -0,0 +1,48 @@
1
+ # @singapore-editor/gutters
2
+
3
+ Line-number and fold-gutter plugins for `@singapore-editor/core`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @singapore-editor/core @singapore-editor/gutters
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Editor } from '@singapore-editor/core/editor'
15
+ import { createFoldGutterPlugin, createLineGutterPlugin } from '@singapore-editor/gutters'
16
+ import '@singapore-editor/core/style.css'
17
+ import '@singapore-editor/gutters/style.css'
18
+
19
+ const editor = new Editor(document.querySelector('#editor')!, {
20
+ plugins: [createLineGutterPlugin(), createFoldGutterPlugin()],
21
+ })
22
+ ```
23
+
24
+ Fold icons accept strings, DOM factories, or a `FoldGutterSvgIcon` descriptor through `icon`,
25
+ `expandedIcon`, and `collapsedIcon`:
26
+
27
+ ```ts
28
+ createFoldGutterPlugin({
29
+ icon: { kind: 'svg', viewBox: '0 0 16 16', path: 'M2 5L8 11L14 5Z' },
30
+ iconClassName: 'fold-icon',
31
+ })
32
+ ```
33
+
34
+ SVG icons use `currentColor` and fill their wrapper. Set its dimensions through `iconClassName`;
35
+ the parent button's `data-editor-fold-state` is `expanded` or `collapsed` for state styling.
36
+ Strings and SVG descriptors support provisional paint snapshots. DOM factories disable fold-gutter
37
+ snapshots because restoring them would require the source fold marker.
38
+
39
+ ## Exports
40
+
41
+ - `createLineGutterPlugin` adds a line-number gutter.
42
+ - `createFoldGutterPlugin` adds fold controls for syntax fold markers.
43
+ - `createLineGutterContribution` and `createFoldGutterContribution` expose the lower-level gutter
44
+ contributions.
45
+ - The gutter passes presses through to the text. A contribution whose cell handles them sets
46
+ `interactive: true`, as the fold gutter does; its cell then takes pointer events, and a press it
47
+ does not claim still places the caret.
48
+ - `@singapore-editor/gutters/style.css` imports both gutter styles.
@@ -0,0 +1,35 @@
1
+ .editor-virtualized-fold-toggle {
2
+ align-items: center;
3
+ appearance: none;
4
+ background: transparent;
5
+ border: 0;
6
+ color: #a1a1aa;
7
+ cursor: pointer;
8
+ display: inline-flex;
9
+ font: inherit;
10
+ height: 14px;
11
+ justify-self: center;
12
+ justify-content: center;
13
+ line-height: 1;
14
+ padding: 0;
15
+ }
16
+
17
+ .editor-virtualized-fold-toggle[hidden] {
18
+ display: inline-flex;
19
+ visibility: hidden;
20
+ }
21
+
22
+ .editor-virtualized-fold-icon {
23
+ align-items: center;
24
+ color: inherit;
25
+ display: inline-flex;
26
+ flex: 0 0 auto;
27
+ font-size: 9px;
28
+ justify-content: center;
29
+ line-height: 1;
30
+ pointer-events: none;
31
+ }
32
+
33
+ .editor-virtualized-fold-toggle:hover {
34
+ color: #e4e4e7;
35
+ }
@@ -0,0 +1,27 @@
1
+ import type { EditorGutterContribution, EditorPlugin } from '@singapore-editor/core/extensions';
2
+ import type { VirtualizedFoldMarker } from '@singapore-editor/core/rendering';
3
+ import './foldGutter.css';
4
+ export type FoldGutterIconContext = {
5
+ readonly document: Document;
6
+ readonly state: 'expanded' | 'collapsed';
7
+ readonly marker: VirtualizedFoldMarker;
8
+ };
9
+ export type FoldGutterSvgIcon = {
10
+ readonly kind: 'svg';
11
+ readonly viewBox: string;
12
+ readonly path: string;
13
+ };
14
+ export type FoldGutterIcon = string | FoldGutterSvgIcon | ((context: FoldGutterIconContext) => string | Node);
15
+ export type FoldGutterPluginOptions = {
16
+ readonly width?: number;
17
+ readonly expandedIndicator?: string;
18
+ readonly collapsedIndicator?: string;
19
+ readonly icon?: FoldGutterIcon;
20
+ readonly expandedIcon?: FoldGutterIcon;
21
+ readonly collapsedIcon?: FoldGutterIcon;
22
+ readonly buttonClassName?: string;
23
+ readonly iconClassName?: string;
24
+ };
25
+ export declare function createFoldGutterPlugin(options?: FoldGutterPluginOptions): EditorPlugin;
26
+ export declare function createFoldGutterContribution(options?: FoldGutterPluginOptions): EditorGutterContribution;
27
+ //# sourceMappingURL=foldGutter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldGutter.d.ts","sourceRoot":"","sources":["../src/foldGutter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EAExB,YAAY,EACb,MAAM,mCAAmC,CAAA;AAC1C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAE7E,OAAO,kBAAkB,CAAA;AAEzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAA;IACxC,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,iBAAiB,GACjB,CAAC,CAAC,OAAO,EAAE,qBAAqB,KAAK,MAAM,GAAG,IAAI,CAAC,CAAA;AAEvD,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,CAAA;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAA;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,cAAc,CAAA;IACvC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAyBD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,YAAY,CAS1F;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,uBAA4B,GACpC,wBAAwB,CAoC1B"}
@@ -0,0 +1,257 @@
1
+ import { addClassName, normalizeNonNegativeNumber, setElementHidden } from "./utils.js";
2
+ import "./foldGutter.css";
3
+ //#region src/foldGutter.ts
4
+ var DEFAULT_FOLD_GUTTER_WIDTH = 10;
5
+ var DEFAULT_EXPANDED_INDICATOR = "v";
6
+ var DEFAULT_COLLAPSED_INDICATOR = ">";
7
+ var FOLD_GUTTER_CELL_CLASS = "editor-virtualized-fold-gutter-cell";
8
+ function createFoldGutterPlugin(options = {}) {
9
+ const contribution = createFoldGutterContribution(options);
10
+ return {
11
+ name: "fold-gutter",
12
+ activate(context) {
13
+ return context.registerGutterContribution(contribution);
14
+ }
15
+ };
16
+ }
17
+ function createFoldGutterContribution(options = {}) {
18
+ const width = normalizeNonNegativeNumber(options.width, DEFAULT_FOLD_GUTTER_WIDTH);
19
+ const renderOptions = {
20
+ expandedIndicator: options.expandedIndicator ?? DEFAULT_EXPANDED_INDICATOR,
21
+ collapsedIndicator: options.collapsedIndicator ?? DEFAULT_COLLAPSED_INDICATOR,
22
+ icon: options.icon,
23
+ expandedIcon: options.expandedIcon,
24
+ collapsedIcon: options.collapsedIcon,
25
+ iconClassName: options.iconClassName
26
+ };
27
+ return {
28
+ id: "fold-gutter",
29
+ interactive: true,
30
+ snapshotRenderer: createFoldSnapshotRenderer(options, renderOptions, width),
31
+ createCell(document) {
32
+ const cell = document.createElement("span");
33
+ const button = createFoldButton(document, options.buttonClassName);
34
+ cell.className = FOLD_GUTTER_CELL_CLASS;
35
+ cell.appendChild(button);
36
+ return cell;
37
+ },
38
+ width() {
39
+ return width;
40
+ },
41
+ updateCell(element, row) {
42
+ const button = foldButtonFromCell(element);
43
+ if (!button) return;
44
+ updateFoldGutterButton(button, row, renderOptions);
45
+ },
46
+ disposeCell(element) {
47
+ const button = foldButtonFromCell(element);
48
+ if (!button) return;
49
+ disposeFoldButton(button);
50
+ }
51
+ };
52
+ }
53
+ function createFoldSnapshotRenderer(options, renderOptions, width) {
54
+ if ([
55
+ options.icon,
56
+ options.expandedIcon,
57
+ options.collapsedIcon
58
+ ].some((icon) => typeof icon === "function")) return void 0;
59
+ return {
60
+ key: JSON.stringify([
61
+ "fold-gutter",
62
+ 1,
63
+ width,
64
+ options.buttonClassName,
65
+ renderOptions
66
+ ]),
67
+ capture: captureFoldPaint,
68
+ restore: (element, paint) => restoreFoldPaint(element, paint, renderOptions)
69
+ };
70
+ }
71
+ function captureFoldPaint(element) {
72
+ const button = foldButtonFromCell(element);
73
+ if (!button) return null;
74
+ if (button.hidden) return "hidden";
75
+ const state = button.dataset.editorFoldState;
76
+ return isFoldGutterState(state) ? state : null;
77
+ }
78
+ function restoreFoldPaint(element, paint, options) {
79
+ const button = foldButtonFromCell(element);
80
+ if (!button || paint !== "hidden" && !isFoldGutterState(paint)) return false;
81
+ hideFoldButton(button);
82
+ if (paint === "hidden") return true;
83
+ const source = resolveFoldIconSource(options, paint);
84
+ if (typeof source.icon === "function") return false;
85
+ const content = createStaticFoldIconContent(button.ownerDocument, source.icon);
86
+ const icon = createFoldIconElement(button.ownerDocument, options.iconClassName);
87
+ appendFoldIconContent(icon, content);
88
+ button.replaceChildren(icon);
89
+ button.dataset.editorFoldState = paint;
90
+ syncFoldIndicatorDataset(button, content);
91
+ setElementHidden(button, false);
92
+ return true;
93
+ }
94
+ function createFoldButton(document, className) {
95
+ const button = document.createElement("button");
96
+ button.className = "editor-virtualized-fold-toggle";
97
+ addClassName(button, className);
98
+ button.type = "button";
99
+ button.hidden = true;
100
+ button.disabled = true;
101
+ button.tabIndex = -1;
102
+ button.addEventListener("mousedown", preventFoldButtonMouseDown);
103
+ button.addEventListener("animationend", clearFoldTransition);
104
+ button.addEventListener("animationcancel", clearFoldTransition);
105
+ return button;
106
+ }
107
+ function foldButtonFromCell(element) {
108
+ return element.querySelector(".editor-virtualized-fold-toggle");
109
+ }
110
+ function disposeFoldButton(button) {
111
+ button.onclick = null;
112
+ button.removeEventListener("mousedown", preventFoldButtonMouseDown);
113
+ button.removeEventListener("animationend", clearFoldTransition);
114
+ button.removeEventListener("animationcancel", clearFoldTransition);
115
+ }
116
+ function updateFoldGutterButton(button, row, options) {
117
+ const marker = row.foldMarker;
118
+ if (!marker) {
119
+ hideFoldButton(button);
120
+ return;
121
+ }
122
+ const state = marker.collapsed ? "collapsed" : "expanded";
123
+ const previousKey = button.dataset.editorFoldKey;
124
+ const previousState = button.dataset.editorFoldState;
125
+ showFoldButton(button, marker.key, state);
126
+ updateFoldTransition(button, previousKey, previousState, marker.key, state);
127
+ renderFoldIconIfNeeded(button, marker, state, options);
128
+ button.onclick = (event) => {
129
+ event.preventDefault();
130
+ event.stopPropagation();
131
+ row.toggleFold(marker);
132
+ };
133
+ }
134
+ function hideFoldButton(button) {
135
+ setElementHidden(button, true);
136
+ if (!button.disabled) button.disabled = true;
137
+ if (button.tabIndex !== -1) button.tabIndex = -1;
138
+ button.onclick = null;
139
+ delete button.dataset.editorFoldKey;
140
+ delete button.dataset.editorFoldState;
141
+ delete button.dataset.editorFoldIndicator;
142
+ delete button.dataset.editorFoldIconSignature;
143
+ delete button.dataset.editorFoldTransition;
144
+ if (button.childNodes.length > 0) button.replaceChildren();
145
+ button.removeAttribute("aria-label");
146
+ }
147
+ function showFoldButton(button, key, state) {
148
+ const label = state === "collapsed" ? "Expand folded region" : "Collapse foldable region";
149
+ setElementHidden(button, false);
150
+ if (button.disabled) button.disabled = false;
151
+ if (button.tabIndex !== 0) button.tabIndex = 0;
152
+ button.dataset.editorFoldKey = key;
153
+ button.dataset.editorFoldState = state;
154
+ button.setAttribute("aria-label", label);
155
+ }
156
+ function renderFoldIconIfNeeded(button, marker, state, options) {
157
+ const source = resolveFoldIconSource(options, state);
158
+ const signature = foldIconSignature(marker, state, source);
159
+ if (button.dataset.editorFoldIconSignature === signature) return;
160
+ const content = createFoldIconContent(button.ownerDocument, marker, state, source.icon);
161
+ const icon = createFoldIconElement(button.ownerDocument, options.iconClassName);
162
+ appendFoldIconContent(icon, content);
163
+ button.replaceChildren(icon);
164
+ button.dataset.editorFoldIconSignature = signature;
165
+ syncFoldIndicatorDataset(button, content);
166
+ }
167
+ function resolveFoldIconSource(options, state) {
168
+ const stateIcon = state === "collapsed" ? options.collapsedIcon : options.expandedIcon;
169
+ if (stateIcon !== void 0) return {
170
+ icon: stateIcon,
171
+ stateSpecific: true
172
+ };
173
+ if (options.icon !== void 0) return {
174
+ icon: options.icon,
175
+ stateSpecific: false
176
+ };
177
+ return {
178
+ icon: state === "collapsed" ? options.collapsedIndicator : options.expandedIndicator,
179
+ stateSpecific: true
180
+ };
181
+ }
182
+ function foldIconSignature(marker, state, source) {
183
+ const stateKey = source.stateSpecific ? state : "shared";
184
+ return `${marker.key}:${stateKey}`;
185
+ }
186
+ function createFoldIconContent(document, marker, state, icon) {
187
+ if (typeof icon === "function") return icon({
188
+ document,
189
+ state,
190
+ marker
191
+ });
192
+ return createStaticFoldIconContent(document, icon);
193
+ }
194
+ function createStaticFoldIconContent(document, icon) {
195
+ if (typeof icon === "string") return icon;
196
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
197
+ svg.setAttribute("viewBox", icon.viewBox);
198
+ svg.setAttribute("width", "100%");
199
+ svg.setAttribute("height", "100%");
200
+ svg.setAttribute("fill", "currentColor");
201
+ svg.setAttribute("focusable", "false");
202
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
203
+ path.setAttribute("d", icon.path);
204
+ svg.appendChild(path);
205
+ return svg;
206
+ }
207
+ function createFoldIconElement(document, className) {
208
+ const icon = document.createElement("span");
209
+ icon.className = "editor-virtualized-fold-icon";
210
+ icon.setAttribute("aria-hidden", "true");
211
+ addClassName(icon, className);
212
+ return icon;
213
+ }
214
+ function appendFoldIconContent(icon, content) {
215
+ if (typeof content === "string") {
216
+ icon.textContent = content;
217
+ return;
218
+ }
219
+ icon.appendChild(content);
220
+ }
221
+ function syncFoldIndicatorDataset(button, content) {
222
+ if (typeof content === "string") {
223
+ button.dataset.editorFoldIndicator = content;
224
+ return;
225
+ }
226
+ delete button.dataset.editorFoldIndicator;
227
+ }
228
+ function updateFoldTransition(button, previousKey, previousState, nextKey, nextState) {
229
+ if (previousKey !== nextKey) {
230
+ delete button.dataset.editorFoldTransition;
231
+ return;
232
+ }
233
+ if (!isFoldGutterState(previousState)) {
234
+ delete button.dataset.editorFoldTransition;
235
+ return;
236
+ }
237
+ if (previousState === nextState) return;
238
+ button.dataset.editorFoldTransition = foldTransitionForState(nextState);
239
+ }
240
+ function foldTransitionForState(state) {
241
+ return state === "collapsed" ? "collapse" : "expand";
242
+ }
243
+ function isFoldGutterState(state) {
244
+ return state === "collapsed" || state === "expanded";
245
+ }
246
+ function preventFoldButtonMouseDown(event) {
247
+ event.preventDefault();
248
+ event.stopPropagation();
249
+ }
250
+ function clearFoldTransition(event) {
251
+ if (!(event.currentTarget instanceof HTMLButtonElement)) return;
252
+ delete event.currentTarget.dataset.editorFoldTransition;
253
+ }
254
+ //#endregion
255
+ export { createFoldGutterContribution, createFoldGutterPlugin };
256
+
257
+ //# sourceMappingURL=foldGutter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldGutter.js","names":[],"sources":["../src/foldGutter.ts"],"sourcesContent":["import type {\n EditorGutterContribution,\n EditorGutterRowContext,\n EditorPlugin,\n} from '@singapore-editor/core/extensions'\nimport type { VirtualizedFoldMarker } from '@singapore-editor/core/rendering'\nimport { addClassName, normalizeNonNegativeNumber, setElementHidden } from './utils'\nimport './foldGutter.css'\n\nexport type FoldGutterIconContext = {\n readonly document: Document\n readonly state: 'expanded' | 'collapsed'\n readonly marker: VirtualizedFoldMarker\n}\n\nexport type FoldGutterSvgIcon = {\n readonly kind: 'svg'\n readonly viewBox: string\n readonly path: string\n}\n\nexport type FoldGutterIcon =\n | string\n | FoldGutterSvgIcon\n | ((context: FoldGutterIconContext) => string | Node)\n\nexport type FoldGutterPluginOptions = {\n readonly width?: number\n readonly expandedIndicator?: string\n readonly collapsedIndicator?: string\n readonly icon?: FoldGutterIcon\n readonly expandedIcon?: FoldGutterIcon\n readonly collapsedIcon?: FoldGutterIcon\n readonly buttonClassName?: string\n readonly iconClassName?: string\n}\n\ntype FoldGutterState = FoldGutterIconContext['state']\n\ntype FoldGutterTransition = 'expand' | 'collapse'\n\ntype FoldGutterIconSource = {\n readonly icon: FoldGutterIcon\n readonly stateSpecific: boolean\n}\n\ntype FoldGutterRenderOptions = {\n readonly expandedIndicator: string\n readonly collapsedIndicator: string\n readonly icon?: FoldGutterIcon\n readonly expandedIcon?: FoldGutterIcon\n readonly collapsedIcon?: FoldGutterIcon\n readonly iconClassName?: string\n}\n\nconst DEFAULT_FOLD_GUTTER_WIDTH = 10\nconst DEFAULT_EXPANDED_INDICATOR = 'v'\nconst DEFAULT_COLLAPSED_INDICATOR = '>'\nconst FOLD_GUTTER_CELL_CLASS = 'editor-virtualized-fold-gutter-cell'\n\nexport function createFoldGutterPlugin(options: FoldGutterPluginOptions = {}): EditorPlugin {\n const contribution = createFoldGutterContribution(options)\n\n return {\n name: 'fold-gutter',\n activate(context) {\n return context.registerGutterContribution(contribution)\n },\n }\n}\n\nexport function createFoldGutterContribution(\n options: FoldGutterPluginOptions = {},\n): EditorGutterContribution {\n const width = normalizeNonNegativeNumber(options.width, DEFAULT_FOLD_GUTTER_WIDTH)\n const renderOptions: FoldGutterRenderOptions = {\n expandedIndicator: options.expandedIndicator ?? DEFAULT_EXPANDED_INDICATOR,\n collapsedIndicator: options.collapsedIndicator ?? DEFAULT_COLLAPSED_INDICATOR,\n icon: options.icon,\n expandedIcon: options.expandedIcon,\n collapsedIcon: options.collapsedIcon,\n iconClassName: options.iconClassName,\n }\n\n return {\n id: 'fold-gutter',\n interactive: true,\n snapshotRenderer: createFoldSnapshotRenderer(options, renderOptions, width),\n createCell(document) {\n const cell = document.createElement('span')\n const button = createFoldButton(document, options.buttonClassName)\n cell.className = FOLD_GUTTER_CELL_CLASS\n cell.appendChild(button)\n return cell\n },\n width() {\n return width\n },\n updateCell(element, row) {\n const button = foldButtonFromCell(element)\n if (!button) return\n updateFoldGutterButton(button, row, renderOptions)\n },\n disposeCell(element) {\n const button = foldButtonFromCell(element)\n if (!button) return\n disposeFoldButton(button)\n },\n }\n}\n\nfunction createFoldSnapshotRenderer(\n options: FoldGutterPluginOptions,\n renderOptions: FoldGutterRenderOptions,\n width: number,\n): EditorGutterContribution['snapshotRenderer'] {\n if (\n [options.icon, options.expandedIcon, options.collapsedIcon].some(\n (icon) => typeof icon === 'function',\n )\n )\n return undefined\n return {\n key: JSON.stringify(['fold-gutter', 1, width, options.buttonClassName, renderOptions]),\n capture: captureFoldPaint,\n restore: (element, paint) => restoreFoldPaint(element, paint, renderOptions),\n }\n}\n\nfunction captureFoldPaint(element: HTMLElement): string | null {\n const button = foldButtonFromCell(element)\n if (!button) return null\n if (button.hidden) return 'hidden'\n const state = button.dataset.editorFoldState\n return isFoldGutterState(state) ? state : null\n}\n\nfunction restoreFoldPaint(\n element: HTMLElement,\n paint: string,\n options: FoldGutterRenderOptions,\n): boolean {\n const button = foldButtonFromCell(element)\n if (!button || (paint !== 'hidden' && !isFoldGutterState(paint))) return false\n hideFoldButton(button)\n if (paint === 'hidden') return true\n const source = resolveFoldIconSource(options, paint)\n if (typeof source.icon === 'function') return false\n const content = createStaticFoldIconContent(button.ownerDocument, source.icon)\n const icon = createFoldIconElement(button.ownerDocument, options.iconClassName)\n appendFoldIconContent(icon, content)\n button.replaceChildren(icon)\n button.dataset.editorFoldState = paint\n syncFoldIndicatorDataset(button, content)\n setElementHidden(button, false)\n return true\n}\n\nfunction createFoldButton(document: Document, className: string | undefined): HTMLButtonElement {\n const button = document.createElement('button')\n button.className = 'editor-virtualized-fold-toggle'\n addClassName(button, className)\n button.type = 'button'\n button.hidden = true\n button.disabled = true\n button.tabIndex = -1\n button.addEventListener('mousedown', preventFoldButtonMouseDown)\n button.addEventListener('animationend', clearFoldTransition)\n button.addEventListener('animationcancel', clearFoldTransition)\n return button\n}\n\nfunction foldButtonFromCell(element: HTMLElement): HTMLButtonElement | null {\n return element.querySelector<HTMLButtonElement>('.editor-virtualized-fold-toggle')\n}\n\nfunction disposeFoldButton(button: HTMLButtonElement): void {\n button.onclick = null\n button.removeEventListener('mousedown', preventFoldButtonMouseDown)\n button.removeEventListener('animationend', clearFoldTransition)\n button.removeEventListener('animationcancel', clearFoldTransition)\n}\n\nfunction updateFoldGutterButton(\n button: HTMLButtonElement,\n row: EditorGutterRowContext,\n options: FoldGutterRenderOptions,\n): void {\n const marker = row.foldMarker\n if (!marker) {\n hideFoldButton(button)\n return\n }\n\n const state = marker.collapsed ? 'collapsed' : 'expanded'\n const previousKey = button.dataset.editorFoldKey\n const previousState = button.dataset.editorFoldState\n showFoldButton(button, marker.key, state)\n updateFoldTransition(button, previousKey, previousState, marker.key, state)\n renderFoldIconIfNeeded(button, marker, state, options)\n button.onclick = (event) => {\n event.preventDefault()\n event.stopPropagation()\n row.toggleFold(marker)\n }\n}\n\nfunction hideFoldButton(button: HTMLButtonElement): void {\n setElementHidden(button, true)\n if (!button.disabled) button.disabled = true\n if (button.tabIndex !== -1) button.tabIndex = -1\n button.onclick = null\n delete button.dataset.editorFoldKey\n delete button.dataset.editorFoldState\n delete button.dataset.editorFoldIndicator\n delete button.dataset.editorFoldIconSignature\n delete button.dataset.editorFoldTransition\n if (button.childNodes.length > 0) button.replaceChildren()\n button.removeAttribute('aria-label')\n}\n\nfunction showFoldButton(button: HTMLButtonElement, key: string, state: FoldGutterState): void {\n const label = state === 'collapsed' ? 'Expand folded region' : 'Collapse foldable region'\n setElementHidden(button, false)\n if (button.disabled) button.disabled = false\n if (button.tabIndex !== 0) button.tabIndex = 0\n button.dataset.editorFoldKey = key\n button.dataset.editorFoldState = state\n button.setAttribute('aria-label', label)\n}\n\nfunction renderFoldIconIfNeeded(\n button: HTMLButtonElement,\n marker: VirtualizedFoldMarker,\n state: FoldGutterState,\n options: FoldGutterRenderOptions,\n): void {\n const source = resolveFoldIconSource(options, state)\n const signature = foldIconSignature(marker, state, source)\n if (button.dataset.editorFoldIconSignature === signature) return\n\n const content = createFoldIconContent(button.ownerDocument, marker, state, source.icon)\n const icon = createFoldIconElement(button.ownerDocument, options.iconClassName)\n appendFoldIconContent(icon, content)\n button.replaceChildren(icon)\n button.dataset.editorFoldIconSignature = signature\n syncFoldIndicatorDataset(button, content)\n}\n\nfunction resolveFoldIconSource(\n options: FoldGutterRenderOptions,\n state: FoldGutterState,\n): FoldGutterIconSource {\n const stateIcon = state === 'collapsed' ? options.collapsedIcon : options.expandedIcon\n if (stateIcon !== undefined) return { icon: stateIcon, stateSpecific: true }\n if (options.icon !== undefined) return { icon: options.icon, stateSpecific: false }\n\n const icon = state === 'collapsed' ? options.collapsedIndicator : options.expandedIndicator\n return { icon, stateSpecific: true }\n}\n\nfunction foldIconSignature(\n marker: VirtualizedFoldMarker,\n state: FoldGutterState,\n source: FoldGutterIconSource,\n): string {\n const stateKey = source.stateSpecific ? state : 'shared'\n return `${marker.key}:${stateKey}`\n}\n\nfunction createFoldIconContent(\n document: Document,\n marker: VirtualizedFoldMarker,\n state: FoldGutterState,\n icon: FoldGutterIcon,\n): string | Node {\n if (typeof icon === 'function') return icon({ document, state, marker })\n return createStaticFoldIconContent(document, icon)\n}\n\nfunction createStaticFoldIconContent(\n document: Document,\n icon: string | FoldGutterSvgIcon,\n): string | SVGSVGElement {\n if (typeof icon === 'string') return icon\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')\n svg.setAttribute('viewBox', icon.viewBox)\n svg.setAttribute('width', '100%')\n svg.setAttribute('height', '100%')\n svg.setAttribute('fill', 'currentColor')\n svg.setAttribute('focusable', 'false')\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')\n path.setAttribute('d', icon.path)\n svg.appendChild(path)\n return svg\n}\n\nfunction createFoldIconElement(document: Document, className: string | undefined): HTMLSpanElement {\n const icon = document.createElement('span')\n icon.className = 'editor-virtualized-fold-icon'\n icon.setAttribute('aria-hidden', 'true')\n addClassName(icon, className)\n return icon\n}\n\nfunction appendFoldIconContent(icon: HTMLSpanElement, content: string | Node): void {\n if (typeof content === 'string') {\n icon.textContent = content\n return\n }\n\n icon.appendChild(content)\n}\n\nfunction syncFoldIndicatorDataset(button: HTMLButtonElement, content: string | Node): void {\n if (typeof content === 'string') {\n button.dataset.editorFoldIndicator = content\n return\n }\n\n delete button.dataset.editorFoldIndicator\n}\n\nfunction updateFoldTransition(\n button: HTMLButtonElement,\n previousKey: string | undefined,\n previousState: string | undefined,\n nextKey: string,\n nextState: FoldGutterState,\n): void {\n if (previousKey !== nextKey) {\n delete button.dataset.editorFoldTransition\n return\n }\n if (!isFoldGutterState(previousState)) {\n delete button.dataset.editorFoldTransition\n return\n }\n if (previousState === nextState) return\n\n button.dataset.editorFoldTransition = foldTransitionForState(nextState)\n}\n\nfunction foldTransitionForState(state: FoldGutterState): FoldGutterTransition {\n return state === 'collapsed' ? 'collapse' : 'expand'\n}\n\nfunction isFoldGutterState(state: string | undefined): state is FoldGutterState {\n return state === 'collapsed' || state === 'expanded'\n}\n\nfunction preventFoldButtonMouseDown(event: MouseEvent): void {\n event.preventDefault()\n event.stopPropagation()\n}\n\nfunction clearFoldTransition(event: Event): void {\n if (!(event.currentTarget instanceof HTMLButtonElement)) return\n delete event.currentTarget.dataset.editorFoldTransition\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { createLineGutterContribution, createLineGutterPlugin, type LineGutterPluginOptions, } from './lineGutter';
2
+ export { createFoldGutterContribution, createFoldGutterPlugin, type FoldGutterIcon, type FoldGutterIconContext, type FoldGutterPluginOptions, type FoldGutterSvgIcon, } from './foldGutter';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createLineGutterContribution, createLineGutterPlugin } from "./lineGutter.js";
2
+ import { createFoldGutterContribution, createFoldGutterPlugin } from "./foldGutter.js";
3
+ export { createFoldGutterContribution, createFoldGutterPlugin, createLineGutterContribution, createLineGutterPlugin };
@@ -0,0 +1,18 @@
1
+ .editor-virtualized-gutter-label {
2
+ color: var(--editor-gutter-foreground);
3
+ font: inherit;
4
+ font-variant-numeric: tabular-nums;
5
+ justify-content: flex-end;
6
+ line-height: inherit;
7
+ padding-left: 2px;
8
+ padding-right: 4px;
9
+ tab-size: var(--editor-tab-size, 4);
10
+ }
11
+
12
+ .editor-virtualized-line-number::before {
13
+ content: counter(editor-line, var(--editor-line-gutter-counter-style, decimal));
14
+ }
15
+
16
+ .editor-virtualized-line-number-active {
17
+ color: var(--editor-gutter-active-foreground);
18
+ }
@@ -0,0 +1,14 @@
1
+ import type { EditorGutterContribution, EditorGutterRowContext, EditorPlugin } from '@singapore-editor/core/extensions';
2
+ import './lineGutter.css';
3
+ export type LineGutterPluginOptions = {
4
+ readonly counterStyle?: string;
5
+ readonly labelForRow?: (row: EditorGutterRowContext) => LineGutterLabel;
6
+ readonly minLabelColumns?: number;
7
+ readonly minDigits?: number;
8
+ readonly minWidth?: number;
9
+ readonly startLine?: number;
10
+ };
11
+ export type LineGutterLabel = number | string | null | undefined;
12
+ export declare function createLineGutterPlugin(options?: LineGutterPluginOptions): EditorPlugin;
13
+ export declare function createLineGutterContribution(options?: LineGutterPluginOptions): EditorGutterContribution;
14
+ //# sourceMappingURL=lineGutter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineGutter.d.ts","sourceRoot":"","sources":["../src/lineGutter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,YAAY,EACb,MAAM,mCAAmC,CAAA;AAE1C,OAAO,kBAAkB,CAAA;AAEzB,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,eAAe,CAAA;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;AAQhE,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,YAAY,CAS1F;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,uBAA4B,GACpC,wBAAwB,CAmD1B"}
@@ -0,0 +1,124 @@
1
+ import { normalizeNonNegativeNumber, normalizePositiveInteger, setElementHidden } from "./utils.js";
2
+ import "./lineGutter.css";
3
+ //#region src/lineGutter.ts
4
+ var DEFAULT_COUNTER_STYLE = "decimal";
5
+ var DEFAULT_LINE_GUTTER_MIN_COLUMNS = 3;
6
+ var DEFAULT_LINE_GUTTER_MIN_WIDTH = 26;
7
+ var DEFAULT_START_LINE = 1;
8
+ var LINE_GUTTER_PADDING_PX = 8;
9
+ function createLineGutterPlugin(options = {}) {
10
+ const contribution = createLineGutterContribution(options);
11
+ return {
12
+ name: "line-gutter",
13
+ activate(context) {
14
+ return context.registerGutterContribution(contribution);
15
+ }
16
+ };
17
+ }
18
+ function createLineGutterContribution(options = {}) {
19
+ const counterStyle = options.counterStyle ?? DEFAULT_COUNTER_STYLE;
20
+ const minLabelColumns = Math.max(normalizePositiveInteger(options.minLabelColumns, DEFAULT_LINE_GUTTER_MIN_COLUMNS), normalizePositiveInteger(options.minDigits, DEFAULT_LINE_GUTTER_MIN_COLUMNS));
21
+ const minWidth = normalizeNonNegativeNumber(options.minWidth, DEFAULT_LINE_GUTTER_MIN_WIDTH);
22
+ const startLine = normalizePositiveInteger(options.startLine, DEFAULT_START_LINE);
23
+ const labelForRow = options.labelForRow ?? null;
24
+ return {
25
+ id: "line-gutter",
26
+ snapshotRenderer: labelForRow ? void 0 : {
27
+ key: JSON.stringify([
28
+ "line-gutter",
29
+ 1,
30
+ counterStyle,
31
+ minLabelColumns,
32
+ minWidth,
33
+ startLine
34
+ ]),
35
+ capture: captureLineGutterPaint,
36
+ restore: restoreLineGutterPaint
37
+ },
38
+ createCell(document) {
39
+ const element = document.createElement("span");
40
+ element.className = labelForRow ? "editor-virtualized-gutter-label editor-virtualized-line-label" : "editor-virtualized-gutter-label editor-virtualized-line-number";
41
+ element.setAttribute("aria-hidden", "true");
42
+ if (!labelForRow) element.style.setProperty("--editor-line-gutter-counter-style", counterStyle);
43
+ return element;
44
+ },
45
+ width(context) {
46
+ const endLine = startLine + context.lineCount - 1;
47
+ const columns = labelForRow ? minLabelColumns : Math.max(minLabelColumns, decimalDigitCount(endLine));
48
+ return Math.max(minWidth, Math.ceil(columns * context.metrics.characterWidth + LINE_GUTTER_PADDING_PX));
49
+ },
50
+ updateCell(element, row) {
51
+ updateLineGutterCell(element, row, startLine, labelForRow);
52
+ }
53
+ };
54
+ }
55
+ function captureLineGutterPaint(element) {
56
+ const counter = element.hidden ? "" : element.style.counterSet;
57
+ if (counter && !/^editor-line [0-9]+$/.test(counter)) return null;
58
+ return JSON.stringify({
59
+ counter,
60
+ hidden: element.hidden,
61
+ active: element.classList.contains("editor-virtualized-line-number-active")
62
+ });
63
+ }
64
+ function restoreLineGutterPaint(element, paint) {
65
+ const parsed = parseLineGutterPaint(paint);
66
+ if (!parsed) return false;
67
+ setElementHidden(element, parsed.hidden);
68
+ setCounterSet(element, parsed.counter);
69
+ element.classList.toggle("editor-virtualized-line-number-active", parsed.active);
70
+ return true;
71
+ }
72
+ function parseLineGutterPaint(paint) {
73
+ if (paint.length > 256) return null;
74
+ try {
75
+ const value = JSON.parse(paint);
76
+ if (value === null || typeof value !== "object") return null;
77
+ if (!("counter" in value) || typeof value.counter !== "string") return null;
78
+ if (!("hidden" in value) || typeof value.hidden !== "boolean") return null;
79
+ if (!("active" in value) || typeof value.active !== "boolean") return null;
80
+ if (!value.hidden && !/^editor-line [0-9]+$/.test(value.counter)) return null;
81
+ if (value.hidden && value.counter !== "") return null;
82
+ return {
83
+ counter: value.counter,
84
+ hidden: value.hidden,
85
+ active: value.active
86
+ };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ function updateLineGutterCell(element, row, startLine, labelForRow) {
92
+ const label = row.primaryText && labelForRow ? labelForRow(row) : null;
93
+ const hidden = !row.primaryText || labelForRow !== null && !hasLineGutterLabel(label);
94
+ setElementHidden(element, hidden);
95
+ element.classList.toggle("editor-virtualized-line-number-active", !hidden && row.cursorLine && row.cursorLineHighlight.gutterNumber);
96
+ if (hidden) {
97
+ if (labelForRow) setTextContent(element, "");
98
+ return;
99
+ }
100
+ if (labelForRow) {
101
+ setTextContent(element, String(label));
102
+ return;
103
+ }
104
+ setCounterSet(element, `editor-line ${startLine + row.bufferRow}`);
105
+ }
106
+ function hasLineGutterLabel(label) {
107
+ if (label === null || label === void 0) return false;
108
+ return String(label).length > 0;
109
+ }
110
+ function setCounterSet(element, value) {
111
+ if (element.style.counterSet === value) return;
112
+ element.style.counterSet = value;
113
+ }
114
+ function setTextContent(element, value) {
115
+ if (element.textContent === value) return;
116
+ element.textContent = value;
117
+ }
118
+ function decimalDigitCount(value) {
119
+ return String(Math.max(1, Math.floor(value))).length;
120
+ }
121
+ //#endregion
122
+ export { createLineGutterContribution, createLineGutterPlugin };
123
+
124
+ //# sourceMappingURL=lineGutter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineGutter.js","names":[],"sources":["../src/lineGutter.ts"],"sourcesContent":["import type {\n EditorGutterContribution,\n EditorGutterRowContext,\n EditorPlugin,\n} from '@singapore-editor/core/extensions'\nimport { normalizeNonNegativeNumber, normalizePositiveInteger, setElementHidden } from './utils'\nimport './lineGutter.css'\n\nexport type LineGutterPluginOptions = {\n readonly counterStyle?: string\n readonly labelForRow?: (row: EditorGutterRowContext) => LineGutterLabel\n readonly minLabelColumns?: number\n readonly minDigits?: number\n readonly minWidth?: number\n readonly startLine?: number\n}\n\nexport type LineGutterLabel = number | string | null | undefined\n\nconst DEFAULT_COUNTER_STYLE = 'decimal'\nconst DEFAULT_LINE_GUTTER_MIN_COLUMNS = 3\nconst DEFAULT_LINE_GUTTER_MIN_WIDTH = 26\nconst DEFAULT_START_LINE = 1\nconst LINE_GUTTER_PADDING_PX = 8\n\nexport function createLineGutterPlugin(options: LineGutterPluginOptions = {}): EditorPlugin {\n const contribution = createLineGutterContribution(options)\n\n return {\n name: 'line-gutter',\n activate(context) {\n return context.registerGutterContribution(contribution)\n },\n }\n}\n\nexport function createLineGutterContribution(\n options: LineGutterPluginOptions = {},\n): EditorGutterContribution {\n const counterStyle = options.counterStyle ?? DEFAULT_COUNTER_STYLE\n const minLabelColumns = Math.max(\n normalizePositiveInteger(options.minLabelColumns, DEFAULT_LINE_GUTTER_MIN_COLUMNS),\n normalizePositiveInteger(options.minDigits, DEFAULT_LINE_GUTTER_MIN_COLUMNS),\n )\n const minWidth = normalizeNonNegativeNumber(options.minWidth, DEFAULT_LINE_GUTTER_MIN_WIDTH)\n const startLine = normalizePositiveInteger(options.startLine, DEFAULT_START_LINE)\n const labelForRow = options.labelForRow ?? null\n\n return {\n id: 'line-gutter',\n snapshotRenderer: labelForRow\n ? undefined\n : {\n key: JSON.stringify([\n 'line-gutter',\n 1,\n counterStyle,\n minLabelColumns,\n minWidth,\n startLine,\n ]),\n capture: captureLineGutterPaint,\n restore: restoreLineGutterPaint,\n },\n createCell(document) {\n const element = document.createElement('span')\n element.className = labelForRow\n ? 'editor-virtualized-gutter-label editor-virtualized-line-label'\n : 'editor-virtualized-gutter-label editor-virtualized-line-number'\n element.setAttribute('aria-hidden', 'true')\n if (!labelForRow) {\n element.style.setProperty('--editor-line-gutter-counter-style', counterStyle)\n }\n return element\n },\n width(context) {\n const endLine = startLine + context.lineCount - 1\n const columns = labelForRow\n ? minLabelColumns\n : Math.max(minLabelColumns, decimalDigitCount(endLine))\n return Math.max(\n minWidth,\n Math.ceil(columns * context.metrics.characterWidth + LINE_GUTTER_PADDING_PX),\n )\n },\n updateCell(element, row) {\n updateLineGutterCell(element, row, startLine, labelForRow)\n },\n }\n}\n\nfunction captureLineGutterPaint(element: HTMLElement): string | null {\n const counter = element.hidden ? '' : element.style.counterSet\n if (counter && !/^editor-line [0-9]+$/.test(counter)) return null\n return JSON.stringify({\n counter,\n hidden: element.hidden,\n active: element.classList.contains('editor-virtualized-line-number-active'),\n })\n}\n\nfunction restoreLineGutterPaint(element: HTMLElement, paint: string): boolean {\n const parsed = parseLineGutterPaint(paint)\n if (!parsed) return false\n setElementHidden(element, parsed.hidden)\n setCounterSet(element, parsed.counter)\n element.classList.toggle('editor-virtualized-line-number-active', parsed.active)\n return true\n}\n\nfunction parseLineGutterPaint(paint: string) {\n if (paint.length > 256) return null\n try {\n const value: unknown = JSON.parse(paint)\n if (value === null || typeof value !== 'object') return null\n if (!('counter' in value) || typeof value.counter !== 'string') return null\n if (!('hidden' in value) || typeof value.hidden !== 'boolean') return null\n if (!('active' in value) || typeof value.active !== 'boolean') return null\n if (!value.hidden && !/^editor-line [0-9]+$/.test(value.counter)) return null\n if (value.hidden && value.counter !== '') return null\n return { counter: value.counter, hidden: value.hidden, active: value.active }\n } catch {\n return null\n }\n}\n\nfunction updateLineGutterCell(\n element: HTMLElement,\n row: EditorGutterRowContext,\n startLine: number,\n labelForRow: ((row: EditorGutterRowContext) => LineGutterLabel) | null,\n): void {\n const label = row.primaryText && labelForRow ? labelForRow(row) : null\n const hidden = !row.primaryText || (labelForRow !== null && !hasLineGutterLabel(label))\n setElementHidden(element, hidden)\n element.classList.toggle(\n 'editor-virtualized-line-number-active',\n !hidden && row.cursorLine && row.cursorLineHighlight.gutterNumber,\n )\n if (hidden) {\n if (labelForRow) setTextContent(element, '')\n return\n }\n\n if (labelForRow) {\n setTextContent(element, String(label))\n return\n }\n\n setCounterSet(element, `editor-line ${startLine + row.bufferRow}`)\n}\n\nfunction hasLineGutterLabel(label: LineGutterLabel): boolean {\n if (label === null || label === undefined) return false\n return String(label).length > 0\n}\n\nfunction setCounterSet(element: HTMLElement, value: string): void {\n if (element.style.counterSet === value) return\n element.style.counterSet = value\n}\n\nfunction setTextContent(element: HTMLElement, value: string): void {\n if (element.textContent === value) return\n element.textContent = value\n}\n\nfunction decimalDigitCount(value: number): number {\n return String(Math.max(1, Math.floor(value))).length\n}\n"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1,2 @@
1
+ @import './lineGutter.css';
2
+ @import './foldGutter.css';
@@ -0,0 +1,5 @@
1
+ export declare function setElementHidden(element: HTMLElement, hidden: boolean): void;
2
+ export declare function addClassName(element: HTMLElement, className: string | undefined): void;
3
+ export declare function normalizePositiveInteger(value: number | undefined, fallback: number): number;
4
+ export declare function normalizeNonNegativeNumber(value: number | undefined, fallback: number): number;
5
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAG5E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAItF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG5F;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9F"}
package/dist/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ //#region src/utils.ts
2
+ function setElementHidden(element, hidden) {
3
+ if (element.hidden === hidden) return;
4
+ element.hidden = hidden;
5
+ }
6
+ function addClassName(element, className) {
7
+ const classNames = className?.split(/\s+/).filter(Boolean) ?? [];
8
+ if (classNames.length === 0) return;
9
+ element.classList.add(...classNames);
10
+ }
11
+ function normalizePositiveInteger(value, fallback) {
12
+ if (!Number.isFinite(value) || value === void 0 || value <= 0) return fallback;
13
+ return Math.floor(value);
14
+ }
15
+ function normalizeNonNegativeNumber(value, fallback) {
16
+ if (!Number.isFinite(value) || value === void 0 || value < 0) return fallback;
17
+ return value;
18
+ }
19
+ //#endregion
20
+ export { addClassName, normalizeNonNegativeNumber, normalizePositiveInteger, setElementHidden };
21
+
22
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["export function setElementHidden(element: HTMLElement, hidden: boolean): void {\n if (element.hidden === hidden) return\n element.hidden = hidden\n}\n\nexport function addClassName(element: HTMLElement, className: string | undefined): void {\n const classNames = className?.split(/\\s+/).filter(Boolean) ?? []\n if (classNames.length === 0) return\n element.classList.add(...classNames)\n}\n\nexport function normalizePositiveInteger(value: number | undefined, fallback: number): number {\n if (!Number.isFinite(value) || value === undefined || value <= 0) return fallback\n return Math.floor(value)\n}\n\nexport function normalizeNonNegativeNumber(value: number | undefined, fallback: number): number {\n if (!Number.isFinite(value) || value === undefined || value < 0) return fallback\n return value\n}\n"],"mappings":";AAAA,SAAgB,iBAAiB,SAAsB,QAAuB;CAC5E,IAAI,QAAQ,WAAW,QAAQ;CAC/B,QAAQ,SAAS;AACnB;AAEA,SAAgB,aAAa,SAAsB,WAAqC;CACtF,MAAM,aAAa,WAAW,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,KAAK,CAAC;CAC/D,IAAI,WAAW,WAAW,GAAG;CAC7B,QAAQ,UAAU,IAAI,GAAG,UAAU;AACrC;AAEA,SAAgB,yBAAyB,OAA2B,UAA0B;CAC5F,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,UAAU,KAAA,KAAa,SAAS,GAAG,OAAO;CACzE,OAAO,KAAK,MAAM,KAAK;AACzB;AAEA,SAAgB,2BAA2B,OAA2B,UAA0B;CAC9F,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,UAAU,KAAA,KAAa,QAAQ,GAAG,OAAO;CACxE,OAAO;AACT"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@singapore-editor/gutters",
3
+ "version": "0.1.1",
4
+ "files": [
5
+ "README.md",
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "sideEffects": [
10
+ "./src/lineGutter.css",
11
+ "./src/foldGutter.css",
12
+ "./src/style.css",
13
+ "./dist/lineGutter.css",
14
+ "./dist/foldGutter.css",
15
+ "./dist/style.css"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./line-gutter": {
24
+ "types": "./dist/lineGutter.d.ts",
25
+ "import": "./dist/lineGutter.js",
26
+ "default": "./dist/lineGutter.js"
27
+ },
28
+ "./fold-gutter": {
29
+ "types": "./dist/foldGutter.d.ts",
30
+ "import": "./dist/foldGutter.js",
31
+ "default": "./dist/foldGutter.js"
32
+ },
33
+ "./line-gutter.css": "./dist/lineGutter.css",
34
+ "./fold-gutter.css": "./dist/foldGutter.css",
35
+ "./style.css": "./dist/style.css"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "scripts": {
42
+ "build": "bun ../../scripts/build-package.ts",
43
+ "test": "vitest run",
44
+ "typecheck": "tsgo --noEmit",
45
+ "lint": "oxlint .",
46
+ "format": "oxfmt --write .",
47
+ "format:check": "oxfmt --check ."
48
+ },
49
+ "dependencies": {
50
+ "@singapore-editor/core": "0.1.2"
51
+ },
52
+ "devDependencies": {
53
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
54
+ "happy-dom": "^20.14.5",
55
+ "oxfmt": "0.70.0",
56
+ "oxlint": "1.85.0",
57
+ "typescript": "~6.0.3",
58
+ "vitest": "^5.0.2"
59
+ }
60
+ }