@kubex/zinc 1.1.164 → 1.1.165

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.164",
3
+ "version": "1.1.165",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -22,6 +22,13 @@ function nextCheckbox(box: string): string {
22
22
  return box === '[_] ' || box === '[*] ' ? '[_] ' : '[ ] ';
23
23
  }
24
24
 
25
+ /** Whether `line` is a list marker with nothing after it — `- `, `2. `, `- [ ] `, or a bare `::`. */
26
+ export function isEmptyListItem(line: string): boolean {
27
+ if (line.trim() === '::') return true;
28
+ const item = LIST_ITEM.exec(line);
29
+ return !!item && line.slice(item[0].length).trim() === '';
30
+ }
31
+
25
32
  /**
26
33
  * What Enter does on a list line: continues the list with the next marker, or ends it by
27
34
  * clearing the marker of an item that has no text. Null when Enter should just insert a
@@ -36,15 +43,14 @@ export function continueList(value: string, start: number, end: number): TextEdi
36
43
  const line = value.slice(lineStart, lineEnd);
37
44
 
38
45
  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;
46
+ if (isEmptyListItem(line)) {
47
+ return {value: value.slice(0, lineStart) + value.slice(lineEnd), caret: lineStart};
48
+ }
42
49
 
43
50
  const item = LIST_ITEM.exec(line);
44
51
  const definition = item ? null : DEFINITION.exec(line);
45
52
  const marker = item?.[0] ?? definition?.[0];
46
53
  if (!marker || start - lineStart < marker.length) return null;
47
- if (item && line.slice(marker.length).trim() === '') return clearLine;
48
54
 
49
55
  if (definition) {
50
56
  // The caret goes before `::` — the term is typed first.
@@ -1,6 +1,6 @@
1
1
  import {ACTION_GROUPS, EDITOR_ACTIONS, matchesShortcut, shortcutLabel, slashItems} from './actions';
2
2
  import {classMap} from "lit/directives/class-map.js";
3
- import {continueList} from './list-continuation';
3
+ import {continueList, isEmptyListItem} from './list-continuation';
4
4
  import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
5
5
  import {defaultValue} from "../../internal/default-value";
6
6
  import {FormControlController} from "../../internal/form";
@@ -562,6 +562,11 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
562
562
  * delimited containers (``` ==== !!!! .... ---- ____ **** ////) as single blocks.
563
563
  */
564
564
  private splitBlocks(source: string): string[] {
565
+ return this.scanBlocks(source).blocks;
566
+ }
567
+
568
+ /** `splitBlocks`, plus whether the source ends inside an unclosed container, where a blank line does not split. */
569
+ private scanBlocks(source: string): { blocks: string[]; open: boolean } {
565
570
  const lines = source.replace(/\r\n/g, '\n').split('\n');
566
571
  const blocks: string[] = [];
567
572
  let current: string[] = [];
@@ -612,7 +617,7 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
612
617
  current.push(line);
613
618
  }
614
619
  push();
615
- return blocks;
620
+ return {blocks, open: fence !== null || depth > 0};
616
621
  }
617
622
 
618
623
  private fenceMarker(line: string): string | null {
@@ -961,6 +966,14 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
961
966
  e.preventDefault();
962
967
  this.activateAction(shortcutAction);
963
968
  } else if (this.isPlainEnter(e)) {
969
+ const finished = this.finishedDraft(input.value, input.selectionStart, input.selectionEnd);
970
+ if (finished !== null) {
971
+ e.preventDefault();
972
+ this.editingDraft = finished;
973
+ this.suppressBlurCommit = true;
974
+ this.insertDraftBlock(this.commitEdit());
975
+ return;
976
+ }
964
977
  const edit = continueList(input.value, input.selectionStart, input.selectionEnd);
965
978
  if (!edit) return;
966
979
  e.preventDefault();
@@ -979,6 +992,20 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
979
992
  }
980
993
  };
981
994
 
995
+ /**
996
+ * The draft to commit when Enter should finish the block — pressed on a trailing blank line or
997
+ * empty list item, outside any container — with that line dropped. Null otherwise.
998
+ */
999
+ private finishedDraft(value: string, start: number, end: number): string | null {
1000
+ if (start !== end || value.slice(start).trim() !== '') return null;
1001
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
1002
+ const line = value.slice(lineStart);
1003
+ if (line.trim() !== '' && !isEmptyListItem(line)) return null;
1004
+ const before = value.slice(0, lineStart);
1005
+ if (before.trim() === '' || this.scanBlocks(before).open) return null;
1006
+ return before.trimEnd();
1007
+ }
1008
+
982
1009
  private isPlainEnter(e: KeyboardEvent): boolean {
983
1010
  return e.key === 'Enter' && !e.isComposing && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey;
984
1011
  }
@@ -595,6 +595,47 @@ Second"></zn-remarkd-editor>`);
595
595
  expect(enter(input).defaultPrevented).to.be.false;
596
596
  });
597
597
 
598
+ /** Opens the first block and replaces its draft with `draft`, caret at the end. */
599
+ async function editDraft(draft: string) {
600
+ const el = await fixture<ZnRemarkdEditor>(html`
601
+ <zn-remarkd-editor value="placeholder"></zn-remarkd-editor>`);
602
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
603
+ await el.updateComplete;
604
+ return {el, input: typeInBlock(el, draft)};
605
+ }
606
+
607
+ it('should finish the block on Enter at an empty trailing list item', async () => {
608
+ const {el, input} = await editDraft('- a\n- ');
609
+ expect(enter(input).defaultPrevented).to.be.true;
610
+ await el.updateComplete;
611
+
612
+ expect(el.value).to.equal('- a');
613
+ const draft = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
614
+ expect(draft.value, 'a fresh block is opened below').to.equal('');
615
+ });
616
+
617
+ it('should finish the block on a double Enter after a paragraph', async () => {
618
+ const {el, input} = await editDraft('hello\n');
619
+ expect(enter(input).defaultPrevented).to.be.true;
620
+ await el.updateComplete;
621
+
622
+ expect(el.value).to.equal('hello');
623
+ expect(el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!.value).to.equal('');
624
+ });
625
+
626
+ it('should keep a blank line inside an open container', async () => {
627
+ const {input} = await editDraft('```\ncode\n');
628
+ expect(enter(input).defaultPrevented).to.be.false;
629
+ });
630
+
631
+ it('should clear a mid-list empty item without finishing the block', async () => {
632
+ const {el, input} = await editDraft('- a\n- \n- b');
633
+ input.setSelectionRange(6, 6);
634
+ expect(enter(input).defaultPrevented).to.be.true;
635
+ await el.updateComplete;
636
+ expect(input.value).to.equal('- a\n\n- b');
637
+ });
638
+
598
639
  it('should continue a list on Enter in the raw source view', async () => {
599
640
  const el = await fixture<ZnRemarkdEditor>(html`
600
641
  <zn-remarkd-editor allow-raw value="- [ ] first"></zn-remarkd-editor>`);