@kubex/zinc 1.1.162 → 1.1.164
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/dist/custom-elements.json +133 -78
- package/dist/vscode.html-custom-data.json +12 -12
- package/dist/web-types.json +30 -27
- package/dist/zn.d.ts +38 -2
- package/dist/zn.min.js +324 -270
- package/package.json +1 -1
- package/src/components/menu/menu.scss +21 -3
- package/src/components/menu-item/menu-item.component.ts +1 -1
- package/src/components/menu-item/menu-item.scss +4 -3
- package/src/components/menu-item/submenu-controller.ts +20 -15
- package/src/components/popup/popup.component.ts +3 -1
- package/src/components/remarkd-editor/actions.ts +36 -9
- package/src/components/remarkd-editor/list-continuation.test.ts +67 -0
- package/src/components/remarkd-editor/list-continuation.ts +73 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +116 -38
- package/src/components/remarkd-editor/remarkd-editor.scss +5 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +149 -31
- package/src/events/events.ts +1 -0
- package/src/events/zn-reposition.ts +7 -0
package/package.json
CHANGED
|
@@ -17,9 +17,27 @@
|
|
|
17
17
|
// Same surface as the shell-header menus below, so every dropdown in the
|
|
18
18
|
// product reads as one material rather than splitting into --zn-panel menus
|
|
19
19
|
// and --zn-color-navigation ones depending on where it was mounted.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
background-color: rgb(var(--zn-color-navigation, 255, 255, 255));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A submenu's border sits on its parent menu's edge border, so the two share one line.
|
|
24
|
+
:host([slot="submenu"]) .menu {
|
|
25
|
+
border-start-start-radius: 0;
|
|
26
|
+
border-end-start-radius: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Set by the submenu controller while this submenu extends past its parent's bottom edge.
|
|
30
|
+
:host([slot="submenu"][data-extends-below]) .menu {
|
|
31
|
+
border-end-start-radius: var(--zn-border-radius);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Set by the submenu controller on the parent menu while a submenu covers that corner.
|
|
35
|
+
:host([data-submenu-top]) .menu {
|
|
36
|
+
border-start-end-radius: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:host([data-submenu-bottom]) .menu {
|
|
40
|
+
border-end-end-radius: 0;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
// A capped menu has to look scrollable, so the bar is always drawn rather than
|
|
@@ -260,7 +260,7 @@ export default class ZnMenuItem extends ZincElement {
|
|
|
260
260
|
</span>` : ''}
|
|
261
261
|
|
|
262
262
|
<span part="submenu-icon" class="menu-item__chevron">
|
|
263
|
-
<zn-icon src=${isRtl ? 'chevron_left' : 'chevron_right'} aria-hidden="true"></zn-icon>
|
|
263
|
+
<zn-icon src=${isRtl ? 'chevron_left' : 'chevron_right'} size="18" aria-hidden="true"></zn-icon>
|
|
264
264
|
</span>
|
|
265
265
|
|
|
266
266
|
${this.submenuController.renderSubmenu()}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
@use "../../wc";
|
|
2
2
|
|
|
3
3
|
:host {
|
|
4
|
+
// Overlaps the parent menu's padding so the submenu's border lands on the parent's.
|
|
5
|
+
--submenu-offset: 1px;
|
|
4
6
|
// Baseline — standalone items (e.g. app/sidebar navigation): primary hover
|
|
5
|
-
--submenu-offset: -2px;
|
|
6
7
|
--color: var(--zn-text-menu-link-color);
|
|
7
8
|
--hover-color: rgb(var(--zn-primary));
|
|
8
9
|
--hover-bg: rgba(var(--zn-color-navigation-highlight), var(--zn-panel-highlight-opacity));
|
|
@@ -239,6 +240,8 @@
|
|
|
239
240
|
|
|
240
241
|
.menu-item--has-submenu .menu-item__chevron {
|
|
241
242
|
display: flex;
|
|
243
|
+
height: auto;
|
|
244
|
+
margin-inline-start: var(--zn-spacing-x-small);
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
:host(:not([role="menuitemcheckbox"])) .menu-item .menu-item__check {
|
|
@@ -251,9 +254,7 @@
|
|
|
251
254
|
display: flex;
|
|
252
255
|
}
|
|
253
256
|
|
|
254
|
-
/* Add elevation and z-index to submenus */
|
|
255
257
|
zn-popup::part(popup) {
|
|
256
|
-
box-shadow: var(--zn-shadow-large);
|
|
257
258
|
z-index: 100;
|
|
258
259
|
margin-left: var(--submenu-offset);
|
|
259
260
|
}
|
|
@@ -210,6 +210,16 @@ export class SubmenuController implements ReactiveController {
|
|
|
210
210
|
|
|
211
211
|
const {left, top, width, height} = menu.getBoundingClientRect();
|
|
212
212
|
|
|
213
|
+
const parentMenu = this.host.parentElement;
|
|
214
|
+
const parentPanel = parentMenu?.shadowRoot?.querySelector('.menu') ?? parentMenu;
|
|
215
|
+
if (parentMenu && parentPanel) {
|
|
216
|
+
const parent = parentPanel.getBoundingClientRect();
|
|
217
|
+
menu.toggleAttribute('data-extends-below', top + height > parent.bottom + 1);
|
|
218
|
+
// Flags the parent's corners the submenu covers, so the parent squares them off.
|
|
219
|
+
parentMenu.toggleAttribute('data-submenu-top', top <= parent.top + 1);
|
|
220
|
+
parentMenu.toggleAttribute('data-submenu-bottom', top + height >= parent.bottom - 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
213
223
|
this.host.style.setProperty('--safe-triangle-submenu-start-x', `${isRtl ? left + width : left}px`);
|
|
214
224
|
this.host.style.setProperty('--safe-triangle-submenu-start-y', `${top}px`);
|
|
215
225
|
this.host.style.setProperty('--safe-triangle-submenu-end-x', `${isRtl ? left + width : left}px`);
|
|
@@ -223,6 +233,10 @@ export class SubmenuController implements ReactiveController {
|
|
|
223
233
|
this.host.requestUpdate();
|
|
224
234
|
}
|
|
225
235
|
}
|
|
236
|
+
if (!state) {
|
|
237
|
+
this.host.parentElement?.removeAttribute('data-submenu-top');
|
|
238
|
+
this.host.parentElement?.removeAttribute('data-submenu-bottom');
|
|
239
|
+
}
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
// Shows the submenu. Supports disabling the opening delay, e.g. for keyboard events that want to set the focus to the
|
|
@@ -244,21 +258,12 @@ export class SubmenuController implements ReactiveController {
|
|
|
244
258
|
|
|
245
259
|
// Calculate the space the top of a menu takes-up, for aligning the popup menu-item with the activating element.
|
|
246
260
|
private updateSkidding(): void {
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const skidding = attrs.reduce((accumulator, attr) => {
|
|
255
|
-
const styleValue: CSSStyleValue = styleMap.get(attr) ?? new CSSUnitValue(0, 'px');
|
|
256
|
-
const unitValue = styleValue instanceof CSSUnitValue ? styleValue : new CSSUnitValue(0, 'px');
|
|
257
|
-
const pxValue = unitValue.to('px');
|
|
258
|
-
return accumulator - pxValue.value;
|
|
259
|
-
}, 0);
|
|
260
|
-
|
|
261
|
-
this.skidding = skidding;
|
|
261
|
+
// zn-menu's padding and border sit on its inner `.menu`, not the host.
|
|
262
|
+
const menu = this.host.parentElement;
|
|
263
|
+
const panel = menu?.shadowRoot?.querySelector('.menu') ?? menu;
|
|
264
|
+
if (!panel) return;
|
|
265
|
+
const style = getComputedStyle(panel);
|
|
266
|
+
this.skidding = -(parseFloat(style.paddingTop) + parseFloat(style.borderTopWidth) + parseFloat(style.marginTop));
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
isExpanded(): boolean {
|
|
@@ -31,7 +31,7 @@ function isVirtualElement(e: unknown): e is VirtualElement {
|
|
|
31
31
|
*
|
|
32
32
|
* @dependency zn-example
|
|
33
33
|
*
|
|
34
|
-
* @event zn-
|
|
34
|
+
* @event zn-reposition - Emitted each time the popup is positioned.
|
|
35
35
|
*
|
|
36
36
|
* @slot - The default slot.
|
|
37
37
|
* @slot example - An example slot.
|
|
@@ -418,6 +418,8 @@ export default class ZnPopup extends ZincElement {
|
|
|
418
418
|
this.popup.showPopover();
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
this.emit('zn-reposition');
|
|
422
|
+
|
|
421
423
|
if (this.arrow) {
|
|
422
424
|
const arrowX = middlewareData.arrow!.x;
|
|
423
425
|
const arrowY = middlewareData.arrow!.y;
|
|
@@ -10,6 +10,12 @@ export interface InlineMark {
|
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** A shortcut on the platform modifier: Cmd on Apple devices, Ctrl elsewhere. */
|
|
14
|
+
export interface Shortcut {
|
|
15
|
+
key: string;
|
|
16
|
+
shift?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export interface EditorAction {
|
|
14
20
|
/** Stable id, used for toolbar keys and tests. */
|
|
15
21
|
key: string;
|
|
@@ -26,6 +32,7 @@ export interface EditorAction {
|
|
|
26
32
|
caretOffset?: number;
|
|
27
33
|
/** Actions that open their own picker instead of inserting text. */
|
|
28
34
|
opens?: 'image' | 'include' | 'link';
|
|
35
|
+
shortcut?: Shortcut;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** Toolbar order, most-used first — the last groups are the first to collapse. */
|
|
@@ -116,23 +123,43 @@ export const EDITOR_ACTIONS: EditorAction[] = [
|
|
|
116
123
|
{key: 'ifeval', label: 'If expression', icon: 'equal@lu', group: 'logic', prefix: 'ifeval::[1 > 0]\n\nendif::[]', caretOffset: 9},
|
|
117
124
|
|
|
118
125
|
// Inline — applied to the selection in the open block
|
|
119
|
-
{key: 'inline-formatting', label: 'Strong', icon: 'bold@lu', group: 'inline', keywords: ['bold'], inline: {before: '**', placeholder: 'text'}},
|
|
120
|
-
{key: 'emphasis', label: 'Emphasis', icon: 'italic@lu', group: 'inline', keywords: ['italic'], inline: {before: '__', placeholder: 'text'}},
|
|
121
|
-
{key: 'inline-code', label: 'Code', icon: 'square-code@lu', group: 'inline', inline: {before: '`', placeholder: 'code'}},
|
|
122
|
-
{key: 'underline', label: 'Underline', icon: 'underline@lu', group: 'inline', inline: {before: '___', placeholder: 'text'}},
|
|
123
|
-
{key: 'strike', label: 'Strikethrough', icon: 'strikethrough@lu', group: 'inline', keywords: ['delete'], inline: {before: '~~', placeholder: 'text'}},
|
|
124
|
-
{key: 'subscript', label: 'Subscript', icon: 'subscript@lu', group: 'inline', inline: {before: '~', placeholder: '2'}},
|
|
125
|
-
{key: 'superscript', label: 'Superscript', icon: 'superscript@lu', group: 'inline', inline: {before: '^', placeholder: '2'}},
|
|
126
|
+
{key: 'inline-formatting', label: 'Strong', icon: 'bold@lu', group: 'inline', keywords: ['bold'], inline: {before: '**', placeholder: 'text'}, shortcut: {key: 'b'}},
|
|
127
|
+
{key: 'emphasis', label: 'Emphasis', icon: 'italic@lu', group: 'inline', keywords: ['italic'], inline: {before: '__', placeholder: 'text'}, shortcut: {key: 'i'}},
|
|
128
|
+
{key: 'inline-code', label: 'Code', icon: 'square-code@lu', group: 'inline', inline: {before: '`', placeholder: 'code'}, shortcut: {key: 'e'}},
|
|
129
|
+
{key: 'underline', label: 'Underline', icon: 'underline@lu', group: 'inline', inline: {before: '___', placeholder: 'text'}, shortcut: {key: 'u'}},
|
|
130
|
+
{key: 'strike', label: 'Strikethrough', icon: 'strikethrough@lu', group: 'inline', keywords: ['delete'], inline: {before: '~~', placeholder: 'text'}, shortcut: {key: 'x', shift: true}},
|
|
131
|
+
{key: 'subscript', label: 'Subscript', icon: 'subscript@lu', group: 'inline', inline: {before: '~', placeholder: '2'}, shortcut: {key: ','}},
|
|
132
|
+
{key: 'superscript', label: 'Superscript', icon: 'superscript@lu', group: 'inline', inline: {before: '^', placeholder: '2'}, shortcut: {key: '.'}},
|
|
126
133
|
{key: 'keyboard', label: 'Keyboard', icon: 'keyboard@lu', group: 'inline', keywords: ['kbd', 'shortcut'], inline: {before: 'kbd:[', after: ']', placeholder: 'Ctrl+C'}},
|
|
127
134
|
{key: 'footnote', label: 'Footnote', icon: 'asterisk@lu', group: 'inline', inline: {before: 'footnote:[', after: ']', placeholder: 'Note'}},
|
|
128
135
|
{key: 'tooltip', label: 'Tooltip', icon: 'message-circle-question-mark@lu', group: 'inline', keywords: ['term'], inline: {before: '{', after: '}(Explanation)', placeholder: 'Term'}},
|
|
129
136
|
{key: 'cross-reference', label: 'Cross reference', icon: 'link-2@lu', group: 'inline', keywords: ['xref'], inline: {before: '<<', after: '>>', placeholder: 'section,Label'}},
|
|
130
|
-
{key: 'links-and-images', label: 'Link', icon: 'link@lu', group: 'inline', inline: {before: '[', after: '](https://)', placeholder: 'Label'}},
|
|
131
|
-
{key: 'document-link', label: 'Link to article', icon: 'file-symlink@lu', group: 'inline', keywords: ['article', 'document', 'kb'], opens: 'link'},
|
|
137
|
+
{key: 'links-and-images', label: 'Link', icon: 'link@lu', group: 'inline', inline: {before: '[', after: '](https://)', placeholder: 'Label'}, shortcut: {key: 'k'}},
|
|
138
|
+
{key: 'document-link', label: 'Link to article', icon: 'file-symlink@lu', group: 'inline', keywords: ['article', 'document', 'kb'], opens: 'link', shortcut: {key: 'k', shift: true}},
|
|
132
139
|
{key: 'passthrough', label: 'Passthrough', icon: 'shield@lu', group: 'inline', keywords: ['raw', 'literal'], inline: {before: 'pass:[', after: ']', placeholder: 'raw'}},
|
|
133
140
|
{key: 'curly-bang-passthrough', label: 'Literal braces', icon: 'braces@lu', group: 'inline', inline: {before: '{!', after: '!}', placeholder: 'raw'}},
|
|
134
141
|
];
|
|
135
142
|
|
|
143
|
+
export function isApple(): boolean {
|
|
144
|
+
// Safari and Firefox have no userAgentData yet.
|
|
145
|
+
const platform = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform
|
|
146
|
+
?? navigator.userAgent;
|
|
147
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function matchesShortcut(e: KeyboardEvent, shortcut: Shortcut): boolean {
|
|
151
|
+
// Ctrl+Alt is AltGr on Windows keyboards, so Alt must be up for the combo to count.
|
|
152
|
+
const mod = isApple() ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey;
|
|
153
|
+
return mod && !e.altKey && e.shiftKey === !!shortcut.shift && e.key.toLowerCase() === shortcut.key;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The shortcut as shown to people, e.g. `⌘⇧X` or `Ctrl+Shift+X`. */
|
|
157
|
+
export function shortcutLabel(shortcut: Shortcut): string {
|
|
158
|
+
const key = shortcut.key.toUpperCase();
|
|
159
|
+
if (isApple()) return `⌘${shortcut.shift ? '⇧' : ''}${key}`;
|
|
160
|
+
return `Ctrl+${shortcut.shift ? 'Shift+' : ''}${key}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
/** The registry as slash menu entries. Picker actions insert nothing; the menu emits their action id. */
|
|
137
164
|
export function slashItems(actions: EditorAction[]): SlashMenuItem[] {
|
|
138
165
|
const label = (id: ActionGroup) => ACTION_GROUPS.find(group => group.id === id)?.label;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {continueList} from './list-continuation';
|
|
2
|
+
import {expect} from '@open-wc/testing';
|
|
3
|
+
|
|
4
|
+
/** Enter at `caret` (the end by default), rendered as the new value with `|` at the caret, or null. */
|
|
5
|
+
function enter(value: string, caret = value.length): string | null {
|
|
6
|
+
const edit = continueList(value, caret, caret);
|
|
7
|
+
return edit ? `${edit.value.slice(0, edit.caret)}|${edit.value.slice(edit.caret)}` : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('continueList', () => {
|
|
11
|
+
it('should continue bullet lists with the same marker, including nested depth', () => {
|
|
12
|
+
expect(enter('- a')).to.equal('- a\n- |');
|
|
13
|
+
expect(enter('* a')).to.equal('* a\n* |');
|
|
14
|
+
expect(enter('-- nested')).to.equal('-- nested\n-- |');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should continue numbered lists with the next number', () => {
|
|
18
|
+
expect(enter('1. hello')).to.equal('1. hello\n2. |');
|
|
19
|
+
expect(enter('9. nine')).to.equal('9. nine\n10. |');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should renumber the items after one inserted mid-list', () => {
|
|
23
|
+
expect(enter('1. a\n2. b\n3. c', 4)).to.equal('1. a\n2. |\n3. b\n4. c');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should continue checklists unticked, keeping the editable kind', () => {
|
|
27
|
+
expect(enter('- [ ] a')).to.equal('- [ ] a\n- [ ] |');
|
|
28
|
+
expect(enter('- [x] done')).to.equal('- [x] done\n- [ ] |');
|
|
29
|
+
expect(enter('- [_] editable')).to.equal('- [_] editable\n- [_] |');
|
|
30
|
+
expect(enter('- [*] editable done')).to.equal('- [*] editable done\n- [_] |');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should continue definition lists with the caret on the new term', () => {
|
|
34
|
+
expect(enter('CPU:: Central processor')).to.equal('CPU:: Central processor\n|:: ');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should end the list when Enter is pressed on an empty item', () => {
|
|
38
|
+
expect(enter('- a\n- ')).to.equal('- a\n|');
|
|
39
|
+
expect(enter('1. a\n2. ')).to.equal('1. a\n|');
|
|
40
|
+
expect(enter('- [ ] a\n- [ ] ')).to.equal('- [ ] a\n|');
|
|
41
|
+
expect(enter('CPU:: x\n:: ')).to.equal('CPU:: x\n|');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should carry the text after the caret into the new item', () => {
|
|
45
|
+
expect(enter('- hello world', 7)).to.equal('- hello\n- |world');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should leave Enter alone outside list items', () => {
|
|
49
|
+
expect(enter('plain text')).to.be.null;
|
|
50
|
+
expect(enter('---')).to.be.null;
|
|
51
|
+
expect(enter('NOTE: hi')).to.be.null;
|
|
52
|
+
expect(enter('include::file[]')).to.be.null;
|
|
53
|
+
expect(enter('ifdef::flag[]')).to.be.null;
|
|
54
|
+
// Caret before the marker ends, e.g. at the start of the line.
|
|
55
|
+
expect(enter('- a', 0)).to.be.null;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should leave Enter alone inside verbatim blocks', () => {
|
|
59
|
+
expect(enter('```\n- a')).to.be.null;
|
|
60
|
+
expect(enter('----\n1. a')).to.be.null;
|
|
61
|
+
expect(enter('```\ncode\n```\n- a')).to.equal('```\ncode\n```\n- a\n- |');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should leave Enter alone with a selection', () => {
|
|
65
|
+
expect(continueList('- abc', 2, 4)).to.be.null;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** A textarea edit: the new value and where the caret lands in it. */
|
|
2
|
+
export interface TextEdit {
|
|
3
|
+
value: string;
|
|
4
|
+
caret: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Nesting depth is the marker's repeat count (`--`, `**`), never indentation.
|
|
8
|
+
const LIST_ITEM = /^(-+|\*+|(\d+)\.) (\[[ xX_*]\] )?/;
|
|
9
|
+
const ORDERED_ITEM = /^(\d+)\. /;
|
|
10
|
+
// `::` must end the line or be followed by a space, so macros (`include::x[]`) never match.
|
|
11
|
+
const DEFINITION = /^[^:\s][^\n]*?::(?: |$)/;
|
|
12
|
+
|
|
13
|
+
// Code, listing and literal blocks — their content is verbatim, so a `- ` there is not a list.
|
|
14
|
+
const VERBATIM_DELIMITER = /^(`{3,}|-{4,}|\.{4,})/;
|
|
15
|
+
|
|
16
|
+
function insideVerbatim(before: string): boolean {
|
|
17
|
+
return before.split('\n').filter(line => VERBATIM_DELIMITER.test(line)).length % 2 === 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A new checklist item starts unticked, keeping the editable/readonly kind of the one above. */
|
|
21
|
+
function nextCheckbox(box: string): string {
|
|
22
|
+
return box === '[_] ' || box === '[*] ' ? '[_] ' : '[ ] ';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What Enter does on a list line: continues the list with the next marker, or ends it by
|
|
27
|
+
* clearing the marker of an item that has no text. Null when Enter should just insert a
|
|
28
|
+
* newline — no list line, a selection, or the caret before the marker.
|
|
29
|
+
*/
|
|
30
|
+
export function continueList(value: string, start: number, end: number): TextEdit | null {
|
|
31
|
+
if (start !== end) return null;
|
|
32
|
+
|
|
33
|
+
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
|
34
|
+
const newline = value.indexOf('\n', start);
|
|
35
|
+
const lineEnd = newline === -1 ? value.length : newline;
|
|
36
|
+
const line = value.slice(lineStart, lineEnd);
|
|
37
|
+
|
|
38
|
+
if (insideVerbatim(value.slice(0, lineStart))) return null;
|
|
39
|
+
const clearLine = {value: value.slice(0, lineStart) + value.slice(lineEnd), caret: lineStart};
|
|
40
|
+
// The bare `:: ` a definition continuation leaves behind.
|
|
41
|
+
if (line.trim() === '::') return clearLine;
|
|
42
|
+
|
|
43
|
+
const item = LIST_ITEM.exec(line);
|
|
44
|
+
const definition = item ? null : DEFINITION.exec(line);
|
|
45
|
+
const marker = item?.[0] ?? definition?.[0];
|
|
46
|
+
if (!marker || start - lineStart < marker.length) return null;
|
|
47
|
+
if (item && line.slice(marker.length).trim() === '') return clearLine;
|
|
48
|
+
|
|
49
|
+
if (definition) {
|
|
50
|
+
// The caret goes before `::` — the term is typed first.
|
|
51
|
+
return {value: `${value.slice(0, start)}\n:: ${value.slice(start).replace(/^ +/, '')}`, caret: start + 1};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [, bullet, ordinal, box] = item!;
|
|
55
|
+
const next = (ordinal ? `${Number(ordinal) + 1}.` : bullet) + ' ' + (box ? nextCheckbox(box) : '');
|
|
56
|
+
const head = `${value.slice(0, start)}\n${next}`;
|
|
57
|
+
const rest = value.slice(start).replace(/^ +/, '');
|
|
58
|
+
const tail = ordinal ? renumber(rest, Number(ordinal) + 2) : rest;
|
|
59
|
+
return {value: head + tail, caret: head.length};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shifts the ordered items that directly follow the split line up by one, so the source keeps
|
|
64
|
+
* counting in step. `text` starts with the rest of the split line, which is left alone.
|
|
65
|
+
*/
|
|
66
|
+
function renumber(text: string, from: number): string {
|
|
67
|
+
const lines = text.split('\n');
|
|
68
|
+
for (let i = 1, n = from; i < lines.length; i++, n++) {
|
|
69
|
+
if (!ORDERED_ITEM.test(lines[i])) break;
|
|
70
|
+
lines[i] = lines[i].replace(ORDERED_ITEM, `${n}. `);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|