Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
- Fixed cell formatting sometimes deleting code at the end of the cell (<https://github.com/quarto-dev/quarto/pull/754>).
- Removed visual/source toggle button from Positron editor action bar. You can still switch between editing modes using the command palette or context menu (<https://github.com/quarto-dev/quarto/pull/896>).
- Improved checkbox list item appearance in the Visual Editor (<https://github.com/quarto-dev/quarto/pull/893>).
- Fix columns2 and columns3 snippets leaving `:::` in the document (<https://github.com/quarto-dev/quarto/pull/899>).
- Add fragment snippet (<https://github.com/quarto-dev/quarto/pull/901>).
- Fixed columns2 and columns3 snippets leaving `:::` in the document (<https://github.com/quarto-dev/quarto/pull/899>).
- Added fragment snippet (<https://github.com/quarto-dev/quarto/pull/901>).
- Fixed a bug where visual editor removed the first nested checkbox (<https://github.com/quarto-dev/quarto/pull/895>).

## 1.128.0 (Release on 2026-01-08)

Expand Down
19 changes: 19 additions & 0 deletions apps/vscode/src/test/examples/nested-checked-list.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-

- [x] hello

- [x] hello

- hello

This is what you get from the Visual Editor when you create a nested list in it:

- <div>

- [x] hello

- [x] hello

- hello

</div>
8 changes: 8 additions & 0 deletions apps/vscode/src/test/quartoDoc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ suite("Quarto basics", function () {

assert.equal(vscode.window.activeTextEditor, editor, 'quarto extension interferes with other files opened in VSCode!');
});

test("Roundtrip doesn't change nested-checked-list.qmd", async function () {
const { doc } = await openAndShowTextDocument("nested-checked-list.qmd");

const { before, after } = await roundtrip(doc);

assert.equal(before, after);
});
});

/**
Expand Down
81 changes: 37 additions & 44 deletions packages/editor/src/nodes/list/list-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import { findParentNodeOfType, NodeWithPos, setTextSelection } from 'prosemirror
import { InputRule, wrappingInputRule } from 'prosemirror-inputrules';

import { ProsemirrorCommand, EditorCommandId } from '../../api/command';
import { PandocToken, mapTokens } from '../../api/pandoc';
import { PandocToken } from '../../api/pandoc';

// custom NodeView that accomodates display / interaction with item check boxes
export class CheckedListItemNodeView implements NodeView {
public readonly dom: HTMLElement;
public readonly contentDOM: HTMLElement;

constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {

// create root li element
this.dom = window.document.createElement('li');
if (node.attrs.tight) {
Expand Down Expand Up @@ -167,7 +167,7 @@ export function checkedListItemInputRule() {
}

export interface InputRuleWithHandler extends InputRule {
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction;
}

// allow users to begin a new checked list by typing [x] or [ ] at the beginning of a line
Expand Down Expand Up @@ -212,50 +212,43 @@ export function fragmentWithCheck(schema: Schema, fragment: Fragment, checked: b
const kCheckedChar = '☒';
const kUncheckedChar = '☐';

export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[] } {
// will set this flag based on inspecting the first Str token
let checked: null | boolean | undefined;
let lastWasChecked = false;

// map tokens
const mappedTokens = mapTokens(tokens, tok => {
// if the last token was checked then strip the next space
if (tok.t === 'Space' && lastWasChecked) {
lastWasChecked = false;
return {
t: 'Str',
c: '',
};
}

// derive 'checked' from first chraracter of first Str token encountered
// if we find checked or unchecked then set the flag and strip off
// the first 2 chraracters (the check and the space after it)
else if (tok.t === 'Str' && checked === undefined) {
let text = tok.c as string;
if (text.charAt(0) === kCheckedChar) {
checked = true;
lastWasChecked = true;
text = text.slice(1);
} else if (text.charAt(0) === kUncheckedChar) {
checked = false;
lastWasChecked = true;
text = text.slice(1);
} else {
checked = null;
}
return {
t: 'Str',
c: text,
};
/**
* example of `tokens`:
* ```json
* {t:"Para",c:[{t:"Str",c:"☒"},{t:"Space"},{t:"Str",c:"example"}]}
* ```
*
* this function takes that and returns:
*
* ```json
* {
* checked: true,
* tokens: {t:"Para",c:[{t:"Str",c:""},{t:"Space"},{t:"Str",c:"example"}]},
* }
* ```
*
* notice that the `☒` character was removed from the first "Str" node.
*/
export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[]; } {
const checkNode = tokens[0]?.c?.[0];
const checkChar = checkNode?.t === 'Str' ? checkNode.c.charAt(0) : '';
const checked = checkChar === kCheckedChar ?
true : checkChar === kUncheckedChar
? false :
null;

const modifiedTokens = structuredClone(tokens);
// if checked, then strip the `☒` or `☐` from the first node and remove the following space (if there is one)
if (checked === true || checked === false) {
if (modifiedTokens[0].c[1].t === 'Space') {
modifiedTokens[0].c.splice(0, 2); // remove the first two tokens
} else {
return tok;
modifiedTokens[0].c.splice(0, 1); // remove the first token
}
});
}

// return
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol

return {
checked: checked !== undefined ? checked : null,
tokens: mappedTokens,
checked,
tokens: modifiedTokens,
};
}
19 changes: 10 additions & 9 deletions packages/editor/src/nodes/list/list-pandoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,24 +86,25 @@ export function readPandocList(nodeType: NodeType, capabilities: ListCapabilitie
writer.logExampleList();
}
}

const children = getChildren(tok);
const attrs = { ...getAttrs(tok),
tight: children.length && children[0].length && children[0][0].t === 'Plain'
};
const attrs = {
...getAttrs(tok),
tight: children.length && children[0].length && children[0][0].t === 'Plain'
};
writer.openNode(nodeType, attrs);
children.forEach((child: PandocToken[]) => {
// setup tokens/attribs for output
let tokens = child;
const childAttrs: { checked: null | boolean } = { checked: null };
const childAttrs: { checked: null | boolean; } = { checked: null };

// special task list processing if the current format supports task lists
if (capabilities.tasks) {
// look for checkbox in first character of child tokens
// if we see it, remove it and set childAttrs.checked as appropriate
const childWithChecked = tokensWithChecked(child);
childAttrs.checked = childWithChecked.checked;
tokens = childWithChecked.tokens;
const { tokens: modifiedTokens, checked } = tokensWithChecked(child);
childAttrs.checked = checked;
tokens = modifiedTokens;
}

// process children
Expand Down Expand Up @@ -165,7 +166,7 @@ function listNodeOptions(node: ProsemirrorNode, capabilities: ListCapabilities):
// (allow case of [paragraph,list] which is just a nested list)
node.forEach(item => {
if (options.tight && item.childCount > 1) {
if (item.childCount > 2 || !isList(item.child(1)) ) {
if (item.childCount > 2 || !isList(item.child(1))) {
options.tight = false;
}
}
Expand Down