Skip to content
Draft
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
1,190 changes: 660 additions & 530 deletions package-lock.json

Large diffs are not rendered by default.

92 changes: 49 additions & 43 deletions packages/myst-common/src/extractParts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Block } from 'myst-spec-ext';
import type { FrontmatterParts, GenericNode, GenericParent } from './types.js';
import type { Content, Block, Root, Parent, Paragraph } from 'myst-spec-ext';
import type { FrontmatterParts } from './types.js';
import { remove } from 'unist-util-remove';
import { selectAll } from 'unist-util-select';
import { copyNode, createId, toText } from './utils.js';
Expand Down Expand Up @@ -30,7 +30,7 @@ function coercePart(part?: string | string[]): string[] {
* Selects the block node(s) based on part (string) or tags (string[]).
* If `part` is a string array, any of the parts will be treated equally.
*/
export function selectBlockParts(tree: GenericParent, part?: string | string[]): Block[] {
export function selectBlockParts(tree: Content | Root, part?: string | string[]): Block[] {
const parts = coercePart(part);
if (parts.length === 0) return [];
const blockParts = selectAll('block', tree).filter((block) => {
Expand Down Expand Up @@ -70,28 +70,32 @@ export function selectFrontmatterParts(
}

function createPartBlock(
children: GenericNode[],
children: Block['children'],
part: string,
opts?: {
removePartData?: boolean;
},
) {
const block: GenericParent = { type: 'block', key: createId(), children };
const block: Block = { type: 'block', key: createId(), children };
if (!opts?.removePartData) {
block.data ??= {};
block.data.part = part;
}
return block;
}

function forcedRemove(tree: GenericParent, test: string) {
function forcedRemove(tree: Content | Root, test: string) {
let success = remove(tree, test);
if (!success) {
success = remove(tree, { cascade: false }, test);
}
return success;
}

function isParent(node: Content | Root): node is Extract<Content | Root, Parent> {
return 'children' in node;
}

/**
* Extract implicit part based on heading name
*
Expand All @@ -103,53 +107,55 @@ function forcedRemove(tree: GenericParent, test: string) {
* Ignores anything that is already part of a block with explicit part.
*/
export function extractImplicitPart(
tree: GenericParent,
tree: Content | Root,
part?: string | string[],
opts?: {
removePartData?: boolean;
},
): GenericParent | undefined {
) {
const parts = coercePart(part);
if (parts.length === 0) return;
let insideImplicitPart = false;
const blockParts: GenericNode[] = [];
let paragraphs: GenericNode[] = [];
tree.children?.forEach((child, index) => {
// Add this paragraph to the part
if (insideImplicitPart && child.type === 'paragraph') {
paragraphs.push(copyNode(child));
child.type = '__part_delete__';
}
// Stop adding things if we didn't just add a paragraph OR we are at the last child
if (child.type !== '__part_delete__' || index === tree.children.length - 1) {
insideImplicitPart = false;
if (paragraphs.length > 0) {
blockParts.push(createPartBlock(paragraphs, parts[0], opts));
paragraphs = [];
selectAll('__part_heading__', tree).forEach((node) => {
node.type = '__part_delete__';
});
const blockParts: Block[] = [];
let paragraphs: Paragraph[] = [];
if (isParent(tree)) {
tree.children?.forEach((child, index) => {
// Add this paragraph to the part
if (insideImplicitPart && child.type === 'paragraph') {
paragraphs.push(copyNode(child));
(child as any).type = '__part_delete__';
}
}
if (child.type === 'block') {
// Do not search blocks already marked explicitly as parts
if (child.data?.part) return;
// Do not recursively search beyond top-level blocks on root node
if (tree.type !== 'root') return;
const blockPartsTree = extractImplicitPart(child as GenericParent, parts);
if (blockPartsTree) blockParts.push(...blockPartsTree.children);
} else if (child.type === 'heading' && parts.includes(toText(child).toLowerCase())) {
// Start adding paragraphs to the part after this heading
insideImplicitPart = true;
child.type = '__part_heading__';
}
});
// Stop adding things if we didn't just add a paragraph OR we are at the last child
if ((child as any).type !== '__part_delete__' || index === tree.children.length - 1) {
insideImplicitPart = false;
if (paragraphs.length > 0) {
blockParts.push(createPartBlock(paragraphs, parts[0], opts));
paragraphs = [];
selectAll('__part_heading__', tree).forEach((node) => {
node.type = '__part_delete__';
});
}
}
if (child.type === 'block') {
// Do not search blocks already marked explicitly as parts
if (child.data?.part) return;
// Do not recursively search beyond top-level blocks on root node
if (tree.type !== 'root') return;
const blockPartsTree = extractImplicitPart(child, parts);
if (blockPartsTree) blockParts.push(...blockPartsTree.children);
} else if (child.type === 'heading' && parts.includes(toText(child).toLowerCase())) {
// Start adding paragraphs to the part after this heading
insideImplicitPart = true;
(child as any).type = '__part_heading__';
}
});
}
// Restore part headings if they did not contain any paragraphs
selectAll('__part_heading__', tree).forEach((node) => {
node.type = 'heading';
});
if (blockParts.length === 0) return;
const partsTree = { type: 'root', children: blockParts } as GenericParent;
const partsTree = { type: 'root', children: blockParts };
forcedRemove(tree, '__part_delete__');
return partsTree;
}
Expand All @@ -160,7 +166,7 @@ export function extractImplicitPart(
* This does not look at parts defined in frontmatter.
*/
export function extractPart(
tree: GenericParent,
tree: Root | Content,
part?: string | string[],
opts?: {
/** Helpful for when we are doing recursions, we don't want to extract the part again. */
Expand All @@ -172,7 +178,7 @@ export function extractPart(
/** Dictionary of part trees, processed from frontmatter */
frontmatterParts?: FrontmatterParts;
},
): GenericParent | undefined {
) {
const partStrings = coercePart(part);
if (partStrings.length === 0) return;
const frontmatterParts = selectFrontmatterParts(opts?.frontmatterParts, part);
Expand Down Expand Up @@ -204,7 +210,7 @@ export function extractPart(
return block;
},
);
const partsTree = { type: 'root', children } as GenericParent;
const partsTree = { type: 'root', children };
// Remove the block parts from the main document, even if frontmatter parts are returned
blockParts.forEach((block) => {
(block as any).type = '__delete__';
Expand Down
7 changes: 3 additions & 4 deletions packages/myst-common/src/indices.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { VFile } from 'vfile';
import type { IndexEntry } from 'myst-spec-ext';
import type { IndexEntry, Node } from 'myst-spec-ext';
import { fileError } from './utils.js';
import type { GenericNode } from './types.js';

export type IndexTypeLists = {
single: string[];
Expand All @@ -15,7 +14,7 @@ export function parseIndexLine(
line: string,
{ single, pair, triple, see, seealso }: IndexTypeLists,
vfile: VFile,
node: GenericNode,
node: Node,
) {
if (line.trim().length === 0) return;
// This splits on unescaped colons
Expand Down Expand Up @@ -67,7 +66,7 @@ function createIndexEntry(
export function createIndexEntries(
{ single, pair, triple, see, seealso }: IndexTypeLists,
vfile: VFile,
node: GenericNode,
node: Node,
) {
const entries: IndexEntry[] = [];
single.forEach((singleEntry) => {
Expand Down
8 changes: 4 additions & 4 deletions packages/myst-common/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin } from 'unified';
import type { Directive, Node, Role } from 'myst-spec';
import type { Directive, Node, Role, Content, Root } from 'myst-spec';
import type { VFile } from 'vfile';
import type * as nbformat from '@jupyterlab/nbformat';
import type { PartialJSONObject } from '@lumino/coreutils';
Expand Down Expand Up @@ -90,7 +90,7 @@ export type RoleData = {
};

export type DirectiveContext = {
parseMyst: (source: string, offset?: number) => GenericParent;
parseMyst: (source: string, offset?: number) => Root;
};

export type DirectiveSpec = {
Expand All @@ -101,7 +101,7 @@ export type DirectiveSpec = {
options?: Record<string, OptionDefinition>;
body?: BodyDefinition;
validate?: (data: DirectiveData, vfile: VFile) => DirectiveData;
run: (data: DirectiveData, vfile: VFile, ctx: DirectiveContext) => GenericNode[];
run: (data: DirectiveData, vfile: VFile, ctx: DirectiveContext) => Content[];
};

export type RoleSpec = {
Expand All @@ -111,7 +111,7 @@ export type RoleSpec = {
options?: Record<string, OptionDefinition>;
body?: BodyDefinition;
validate?: (data: RoleData, vfile: VFile) => RoleData;
run: (data: RoleData, vfile: VFile) => GenericNode[];
run: (data: RoleData, vfile: VFile) => Content[];
};

type Select = (selector: string, tree?: GenericParent) => GenericNode | null;
Expand Down
51 changes: 28 additions & 23 deletions packages/myst-common/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { VFile } from 'vfile';
import type { VFileMessage } from 'vfile-message';
import type { Position } from 'unist';
import { customAlphabet } from 'nanoid';
import type { Node, Parent, PhrasingContent } from 'myst-spec';
import type { Node, Parent, PhrasingContent, Content } from 'myst-spec';
import type { RuleId } from './ruleids.js';
import type { AdmonitionKind, GenericNode, GenericParent } from './types.js';

Expand Down Expand Up @@ -153,7 +153,7 @@ export function liftChildren(tree: GenericParent | GenericNode, removeType: stri
}

export function setTextAsChild(node: Partial<Parent>, text: string) {
node.children = [{ type: 'text', value: text } as Node];
node.children = [{ type: 'text', value: text } as Content];
}

/**
Expand All @@ -163,7 +163,7 @@ export function setTextAsChild(node: Partial<Parent>, text: string) {
* @returns A string. An empty string is returned in case no
* textual representation could be extracted.
*/
export function toText(content?: Node[] | Node | null): string {
export function toText(content?: Content[] | Content | null): string {
if (!content) return '';
if (!Array.isArray(content)) return toText([content]);
return (content as PhrasingContent[])
Expand All @@ -179,27 +179,32 @@ export function toText(content?: Node[] | Node | null): string {
export function copyNode<T extends Node | Node[]>(node: T): T {
return structuredClone(node);
}
function nodeHasChildren(node: Content): node is Extract<Content, Parent> {
return 'children' in node;
}

export function mergeTextNodes(node: GenericNode): GenericNode {
const children = node.children?.reduce((c, n) => {
if (n?.type !== 'text') {
c.push(mergeTextNodes(n));
return c;
}
const last = c[c.length - 1];
if (last?.type !== 'text') {
c.push(n);
return c;
}
if (n.position?.end) {
if (!last.position) last.position = {} as Required<GenericNode>['position'];
last.position.end = n.position.end;
}
if (!last.value) last.value = '';
if (n.value) last.value += n.value;
return c;
}, [] as GenericNode[]);
if (children) node.children = children;
export function mergeTextNodes(node: Content): Content {
const children = nodeHasChildren(node)
? node.children.reduce((c, n) => {
if (n?.type !== 'text') {
c.push(mergeTextNodes(n));
return c;
}
const last = c[c.length - 1];
if (last?.type !== 'text') {
c.push(n);
return c;
}
if (n.position?.end) {
if (!last.position) last.position = {} as Required<Node>['position'];
last.position.end = n.position.end;
}
if (!last.value) last.value = '';
if (n.value) last.value += n.value;
return c;
}, [] as Content[])
: [];
if (children) (node as Parent).children = children;
return node;
Comment on lines +186 to 208
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suspect this should actually be generic, e.g.

export function mergeTextNodes<T extends Nodes>(node: T): T {

although that won't be a lower bound

}

Expand Down
4 changes: 2 additions & 2 deletions packages/myst-directives/src/aside.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common';
import type { Aside } from 'myst-spec-ext';
import type { FlowContent, ListContent, PhrasingContent } from 'myst-spec';
import type { FlowContent } from 'myst-spec';
import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js';

export const asideDirective: DirectiveSpec = {
Expand All @@ -18,7 +18,7 @@ export const asideDirective: DirectiveSpec = {
required: true,
},
run(data: DirectiveData): GenericNode[] {
const children = [...(data.body as unknown as (FlowContent | ListContent | PhrasingContent)[])];
const children = [...(data.body as unknown as FlowContent[])];
if (data.arg) {
children.unshift({
type: 'admonitionTitle',
Expand Down
6 changes: 3 additions & 3 deletions packages/myst-parser/tests/myst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ const SKIP_TESTS = [
];

// TODO: import this from myst-spec properly!
const directory = fs.existsSync('../../node_modules/myst-spec/dist/examples')
? '../../node_modules/myst-spec/dist/examples'
: '../../../node_modules/myst-spec/dist/examples';
const directory = fs.existsSync('../../node_modules/myst-spec/docs/examples')
? '../../node_modules/myst-spec/docs/examples'
: '../../../node_modules/myst-spec/docs/examples';

const files: string[] = fs.readdirSync(directory).filter((name) => name.endsWith('.yml'));

Expand Down
7 changes: 4 additions & 3 deletions packages/myst-roles/src/abbreviation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RoleSpec, RoleData, GenericNode } from 'myst-common';
import type { RoleSpec, RoleData } from 'myst-common';
import { addCommonRoleOptions, commonRoleOptions } from './utils.js';
import type { Abbreviation } from 'myst-spec';

const ABBR_PATTERN = /^(.+?)\(([^()]+)\)$/; // e.g. 'CSS (Cascading Style Sheets)'

Expand All @@ -11,12 +12,12 @@ export const abbreviationRole: RoleSpec = {
type: String,
required: true,
},
run(data: RoleData): GenericNode[] {
run(data: RoleData) {
const body = data.body as string;
const match = ABBR_PATTERN.exec(body);
const value = match?.[1]?.trim() ?? body.trim();
const title = match?.[2]?.trim();
const abbr = { type: 'abbreviation', title, children: [{ type: 'text', value }] };
const abbr: Abbreviation = { type: 'abbreviation', title, children: [{ type: 'text', value }] };
addCommonRoleOptions(data, abbr);
return [abbr];
},
Expand Down
7 changes: 4 additions & 3 deletions packages/myst-roles/src/chem.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RoleSpec, RoleData, GenericNode } from 'myst-common';
import type { RoleSpec, RoleData } from 'myst-common';
import { addCommonRoleOptions, commonRoleOptions } from './utils.js';
import type { ChemicalFormula } from 'myst-spec';

export const chemRole: RoleSpec = {
name: 'chemicalFormula',
Expand All @@ -9,8 +10,8 @@ export const chemRole: RoleSpec = {
type: String,
required: true,
},
run(data: RoleData): GenericNode[] {
const chem = { type: 'chemicalFormula', value: data.body as string };
run(data: RoleData) {
const chem: ChemicalFormula = { type: 'chemicalFormula', value: data.body as string };
addCommonRoleOptions(data, chem);
return [chem];
},
Expand Down
1 change: 1 addition & 0 deletions packages/myst-roles/src/cite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const citeRole: RoleSpec = {
kind,
label: label ?? l,
identifier,
children: [],
};
if (data.name.startsWith('cite:year')) {
cite.partial = 'year';
Expand Down
Loading
Loading