You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
5.8 KiB
206 lines
5.8 KiB
export const WORDS_DOCUMENT_VERSION = 1;
|
|
|
|
export type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
|
|
export type WordsBlockType = 'paragraph' | 'heading' | 'quote' | 'list';
|
|
export type WordsListKind = 'ordered' | 'unordered' | 'check';
|
|
export type WordsHeadingLevel = 1 | 2 | 3;
|
|
export type WordsTextAlign = 'left' | 'center' | 'right' | 'justify';
|
|
|
|
export interface WordsDocument {
|
|
readonly version: typeof WORDS_DOCUMENT_VERSION;
|
|
readonly children: readonly WordsBlock[];
|
|
}
|
|
|
|
export type WordsBlock = WordsParagraphBlock | WordsHeadingBlock | WordsQuoteBlock | WordsListBlock;
|
|
|
|
export interface WordsParagraphBlock {
|
|
readonly type: 'paragraph';
|
|
readonly children: readonly WordsInline[];
|
|
readonly textAlign?: WordsTextAlign;
|
|
}
|
|
|
|
export interface WordsHeadingBlock {
|
|
readonly type: 'heading';
|
|
readonly level: WordsHeadingLevel;
|
|
readonly children: readonly WordsInline[];
|
|
readonly textAlign?: WordsTextAlign;
|
|
}
|
|
|
|
export interface WordsQuoteBlock {
|
|
readonly type: 'quote';
|
|
readonly children: readonly WordsInline[];
|
|
readonly textAlign?: WordsTextAlign;
|
|
}
|
|
|
|
export interface WordsListBlock {
|
|
readonly type: 'list';
|
|
readonly kind: WordsListKind;
|
|
readonly children: readonly WordsListItem[];
|
|
}
|
|
|
|
export interface WordsListItem {
|
|
readonly type: 'list-item';
|
|
readonly checked?: boolean;
|
|
readonly indent?: number;
|
|
readonly children: readonly WordsInline[];
|
|
}
|
|
|
|
export type WordsInline = WordsText | WordsLink;
|
|
|
|
export interface WordsText {
|
|
readonly type: 'text';
|
|
readonly text: string;
|
|
readonly marks?: readonly WordsMark[];
|
|
}
|
|
|
|
export interface WordsLink {
|
|
readonly type: 'link';
|
|
readonly href: string;
|
|
readonly title?: string;
|
|
readonly children: readonly WordsInline[];
|
|
}
|
|
|
|
export const WORDS_MARKS = [
|
|
'bold',
|
|
'italic',
|
|
'underline',
|
|
'strike',
|
|
'code'
|
|
] as const satisfies readonly WordsMark[];
|
|
export const WORDS_LIST_KINDS = [
|
|
'ordered',
|
|
'unordered',
|
|
'check'
|
|
] as const satisfies readonly WordsListKind[];
|
|
export const WORDS_HEADING_LEVELS = [1, 2, 3] as const satisfies readonly WordsHeadingLevel[];
|
|
export const WORDS_TEXT_ALIGNS = ['left', 'center', 'right', 'justify'] as const satisfies readonly WordsTextAlign[];
|
|
|
|
export function createEmptyWordsDocument(): WordsDocument {
|
|
return {
|
|
version: WORDS_DOCUMENT_VERSION,
|
|
children: [createParagraph()]
|
|
};
|
|
}
|
|
|
|
export function createParagraph(
|
|
children: readonly WordsInline[] = [createText('')],
|
|
textAlign?: WordsTextAlign
|
|
): WordsParagraphBlock {
|
|
return { type: 'paragraph', children, ...(textAlign ? { textAlign } : {}) };
|
|
}
|
|
|
|
export function createHeading(
|
|
level: WordsHeadingLevel,
|
|
children: readonly WordsInline[] = [createText('')],
|
|
textAlign?: WordsTextAlign
|
|
): WordsHeadingBlock {
|
|
return { type: 'heading', level, children, ...(textAlign ? { textAlign } : {}) };
|
|
}
|
|
|
|
export function createQuote(
|
|
children: readonly WordsInline[] = [createText('')],
|
|
textAlign?: WordsTextAlign
|
|
): WordsQuoteBlock {
|
|
return { type: 'quote', children, ...(textAlign ? { textAlign } : {}) };
|
|
}
|
|
|
|
export function createList(
|
|
kind: WordsListKind,
|
|
children: readonly WordsListItem[] = [createListItem()]
|
|
): WordsListBlock {
|
|
return { type: 'list', kind, children };
|
|
}
|
|
|
|
export function createListItem(
|
|
children: readonly WordsInline[] = [createText('')],
|
|
checked = false,
|
|
indent?: number
|
|
): WordsListItem {
|
|
return {
|
|
type: 'list-item',
|
|
...(checked ? { checked } : {}),
|
|
...(indent ? { indent } : {}),
|
|
children
|
|
};
|
|
}
|
|
|
|
export function createText(text: string, marks: readonly WordsMark[] = []): WordsText {
|
|
const normalizedMarks = normalizeMarks(marks);
|
|
return {
|
|
type: 'text',
|
|
text,
|
|
...(normalizedMarks.length ? { marks: normalizedMarks } : {})
|
|
};
|
|
}
|
|
|
|
export function createLink(
|
|
href: string,
|
|
children: readonly WordsInline[],
|
|
title?: string
|
|
): WordsLink {
|
|
return {
|
|
type: 'link',
|
|
href,
|
|
...(title ? { title } : {}),
|
|
children
|
|
};
|
|
}
|
|
|
|
export function normalizeMarks(marks: readonly WordsMark[] | undefined): readonly WordsMark[] {
|
|
if (!marks?.length) return [];
|
|
const order = new Map<WordsMark, number>(WORDS_MARKS.map((mark, index) => [mark, index]));
|
|
return Array.from(new Set(marks)).sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
}
|
|
|
|
export function sameMarks(
|
|
a: readonly WordsMark[] | undefined,
|
|
b: readonly WordsMark[] | undefined
|
|
): boolean {
|
|
const left = normalizeMarks(a);
|
|
const right = normalizeMarks(b);
|
|
if (left.length !== right.length) return false;
|
|
return left.every((mark, index) => mark === right[index]);
|
|
}
|
|
|
|
export function isWordsMark(value: unknown): value is WordsMark {
|
|
return typeof value === 'string' && (WORDS_MARKS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function isWordsHeadingLevel(value: unknown): value is WordsHeadingLevel {
|
|
return typeof value === 'number' && (WORDS_HEADING_LEVELS as readonly number[]).includes(value);
|
|
}
|
|
|
|
export function isWordsListKind(value: unknown): value is WordsListKind {
|
|
return typeof value === 'string' && (WORDS_LIST_KINDS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function isWordsTextAlign(value: unknown): value is WordsTextAlign {
|
|
return typeof value === 'string' && (WORDS_TEXT_ALIGNS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function isTextInline(inline: WordsInline): inline is WordsText {
|
|
return inline.type === 'text';
|
|
}
|
|
|
|
export function isLinkInline(inline: WordsInline): inline is WordsLink {
|
|
return inline.type === 'link';
|
|
}
|
|
|
|
export function getBlockText(block: WordsBlock): string {
|
|
if (block.type === 'list') {
|
|
return block.children.map((item) => item.children.map(getInlineText).join('')).join('\n');
|
|
}
|
|
return block.children.map(getInlineText).join('');
|
|
}
|
|
|
|
export function getInlineText(inline: WordsInline): string {
|
|
return getInlineTextSafe(inline, new WeakSet<object>());
|
|
}
|
|
|
|
function getInlineTextSafe(inline: WordsInline, seen: WeakSet<object>): string {
|
|
if (seen.has(inline)) return '';
|
|
seen.add(inline);
|
|
if (inline.type === 'text') return inline.text;
|
|
return inline.children.map((child) => getInlineTextSafe(child, seen)).join('');
|
|
}
|