2026-06-04 18:50:07 +00:00
|
|
|
import { CachedMetadata, ListItemCache, Loc, Pos } from 'obsidian';
|
2026-06-04 16:43:52 +00:00
|
|
|
|
|
|
|
|
type ListItem = {
|
2026-06-04 18:50:07 +00:00
|
|
|
type: 'item';
|
|
|
|
|
text: string;
|
|
|
|
|
checkbox: 'x' | ' ' | undefined;
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ListBlock = {
|
2026-06-04 18:50:07 +00:00
|
|
|
type: 'list';
|
|
|
|
|
items: (ListItem | ListBlock)[];
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ListBlockItem = {
|
2026-06-04 18:50:07 +00:00
|
|
|
type: 'listitem';
|
|
|
|
|
text: string;
|
|
|
|
|
checkbox: ' ' | 'x' | undefined;
|
|
|
|
|
depth: number;
|
|
|
|
|
parent: number;
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const makeListItems = (
|
2026-06-04 18:50:07 +00:00
|
|
|
list: ListBlock,
|
|
|
|
|
startingLine: number,
|
|
|
|
|
depth: number = 0
|
2026-06-04 16:43:52 +00:00
|
|
|
): ListBlockItem[] => {
|
2026-06-04 18:50:07 +00:00
|
|
|
if (startingLine === 0) {
|
|
|
|
|
startingLine = 1;
|
|
|
|
|
}
|
|
|
|
|
const lines: ListBlockItem[] = [];
|
|
|
|
|
for (const i of list.items) {
|
|
|
|
|
if (i.type === 'item') {
|
|
|
|
|
lines.push({
|
|
|
|
|
type: 'listitem',
|
|
|
|
|
text: i.text,
|
|
|
|
|
checkbox: i.checkbox,
|
|
|
|
|
depth,
|
|
|
|
|
parent: depth === 0 ? -startingLine : startingLine
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
lines.push(...makeListItems(i, startingLine + lines.length - 1, depth + 1));
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
2026-06-04 18:50:07 +00:00
|
|
|
}
|
|
|
|
|
return lines;
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type FileBlock =
|
2026-06-04 18:50:07 +00:00
|
|
|
| {
|
|
|
|
|
type: 'heading';
|
|
|
|
|
text: string;
|
|
|
|
|
level: number;
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
2026-06-04 18:50:07 +00:00
|
|
|
| ListBlock
|
|
|
|
|
| { type: 'frontmatter'; key: string; text: unknown }
|
|
|
|
|
| { type: 'text'; text: string };
|
|
|
|
|
|
|
|
|
|
const makeFile = (lines: FileBlock[], tabChars = ' '.repeat(4)): [string, CachedMetadata] => {
|
|
|
|
|
let lineNum = 0;
|
|
|
|
|
let content = '';
|
|
|
|
|
|
|
|
|
|
const appendLine = (line: string): Pos => {
|
|
|
|
|
const start: Loc = { line: lineNum, col: 0, offset: content.length };
|
|
|
|
|
content += `${line}\n`;
|
|
|
|
|
const end: Loc = {
|
|
|
|
|
line: lineNum++,
|
|
|
|
|
col: line.length,
|
|
|
|
|
offset: content.length - 1
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
2026-06-04 18:50:07 +00:00
|
|
|
return { start, end };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const meta: CachedMetadata = {};
|
|
|
|
|
|
|
|
|
|
const frontmatter = lines.flatMap(l => (l.type === 'frontmatter' ? l : []));
|
|
|
|
|
|
|
|
|
|
if (frontmatter.length > 0) {
|
|
|
|
|
const data: Record<string, unknown> = {};
|
|
|
|
|
const { start } = appendLine('---');
|
|
|
|
|
for (const elt of frontmatter) {
|
|
|
|
|
if (Array.isArray(elt.text)) {
|
|
|
|
|
appendLine(`${elt.key}: [${(elt.text as unknown[]).join(',')}]`);
|
|
|
|
|
} else {
|
|
|
|
|
appendLine(`${elt.key}: ${String(elt.text)}`);
|
|
|
|
|
}
|
|
|
|
|
data[elt.key] = elt.text;
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
2026-06-04 18:50:07 +00:00
|
|
|
const { end } = appendLine('---');
|
|
|
|
|
meta.frontmatter = {
|
|
|
|
|
position: { start, end },
|
|
|
|
|
...data
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const blocks = lines.flatMap(l => (l.type !== 'frontmatter' ? l : []));
|
|
|
|
|
for (const block of blocks) {
|
|
|
|
|
switch (block.type) {
|
|
|
|
|
case 'heading': {
|
|
|
|
|
if (!meta.headings) {
|
|
|
|
|
meta.headings = [];
|
|
|
|
|
}
|
|
|
|
|
const position = appendLine('#'.repeat(block.level) + ' ' + block.text);
|
|
|
|
|
meta.headings.push({
|
|
|
|
|
position,
|
|
|
|
|
heading: block.text,
|
|
|
|
|
level: block.level
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'list': {
|
|
|
|
|
if (!meta.listItems) {
|
|
|
|
|
meta.listItems = [];
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
2026-06-04 18:50:07 +00:00
|
|
|
for (const item of makeListItems(block, lineNum)) {
|
|
|
|
|
const indent = tabChars.repeat(item.depth);
|
|
|
|
|
const position = appendLine(
|
|
|
|
|
indent + '- ' + (item.checkbox ? `[${item.checkbox}] ` : '') + item.text
|
|
|
|
|
);
|
|
|
|
|
if (indent.length > 0) {
|
|
|
|
|
position.start.col += indent.length - 2;
|
|
|
|
|
position.start.offset += indent.length - 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const listItem: ListItemCache = {
|
|
|
|
|
position,
|
|
|
|
|
parent: item.parent
|
|
|
|
|
};
|
|
|
|
|
if (item.checkbox) {
|
|
|
|
|
listItem['task'] = item.checkbox;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
meta.listItems.push(listItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'text':
|
|
|
|
|
appendLine(block.text);
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
2026-06-04 18:50:07 +00:00
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
return [content, meta];
|
2026-06-04 16:43:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class ListBuilder {
|
2026-06-04 18:50:07 +00:00
|
|
|
items: (ListItem | ListBlock)[] = [];
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
constructor(items: (ListItem | ListBlock)[] = []) {
|
|
|
|
|
this.items = items;
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
item(text: string, checkbox?: boolean) {
|
|
|
|
|
const i: ListItem = {
|
|
|
|
|
type: 'item',
|
|
|
|
|
text,
|
|
|
|
|
checkbox: checkbox === true ? 'x' : checkbox === false ? ' ' : undefined
|
|
|
|
|
};
|
|
|
|
|
return new ListBuilder([...this.items, i]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
list(lb: ListBuilder) {
|
|
|
|
|
return new ListBuilder([...this.items, lb.done()]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
done(): ListBlock {
|
|
|
|
|
return { type: 'list', items: this.items };
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class FileBuilder {
|
2026-06-04 18:50:07 +00:00
|
|
|
private lines: FileBlock[];
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
constructor(lines: FileBlock[] = []) {
|
|
|
|
|
this.lines = lines;
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
frontmatter(frontmatter: Record<string, unknown>): FileBuilder {
|
|
|
|
|
const frontmatterLines = Object.entries(frontmatter).map(
|
|
|
|
|
([k, v]): FileBlock => ({ type: 'frontmatter', key: k, text: v })
|
|
|
|
|
);
|
|
|
|
|
return new FileBuilder([...this.lines, ...frontmatterLines]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
heading(level: number, text: string): FileBuilder {
|
|
|
|
|
return new FileBuilder([...this.lines, { type: 'heading', level, text }]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
text(text: string): FileBuilder {
|
|
|
|
|
return new FileBuilder([...this.lines, { type: 'text', text }]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
list(list: ListBuilder): FileBuilder {
|
|
|
|
|
return new FileBuilder([...this.lines, list.done()]);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
|
2026-06-04 18:50:07 +00:00
|
|
|
done() {
|
|
|
|
|
return makeFile(this.lines);
|
|
|
|
|
}
|
2026-06-04 16:43:52 +00:00
|
|
|
}
|