UNPKG

@udraft/cursor

Version:

uDraft Cursor enables you to declarative write code generators!

382 lines (327 loc) 9.87 kB
import { CursorSelection, CursorTransformer, CursorTransformerMeta, } from "../types/cursor"; export class UCursor<Type extends Record<string, any>> { _children: { key: keyof Type; each: boolean; cursor: UCursor<Type[keyof Type] | Type[keyof Type][number]>; }[] = []; _identationSize: number | null = null; _identationChar: string | null = null; _block: [string, string] | null = null; _prefix: string = ""; _suffix: string = ""; _move: number = 0; _selector: RegExp | null = null; _parent: UCursor<{ [key: string]: Type }> | null = null; _transformers: { key: keyof Type; each: boolean; transformer: | CursorTransformer<Type, any> | CursorTransformer<Type[keyof Type], number>; }[] = []; _template: string = ""; _clearSelections = false; _eachJoin: string = ""; constructor() {} $children() { return [...this._children]; } $parent() { return this._parent; } $blocks(root: boolean = false) { let parent = this.$parent(); let blocks = 0; if (!root && this._block) blocks += 1; if (parent) blocks += parent.$blocks(); blocks += this._move; if (blocks < 0) blocks = 0; return blocks; } $identationChar(): string | null { let parent = this.$parent(); if (parent && this._identationChar === null) return parent.$identationChar(); return this._identationChar; } $identationSize(): number | null { let parent = this.$parent(); if (parent && this._identationSize === null) return parent.$identationSize(); return this._identationSize; } $identation() { let identationSize = this.$identationSize(); let identationChar = this.$identationChar(); return { root: "".padStart( this.$blocks(true) * (identationSize ?? 0), identationChar ?? "" ), children: "".padStart( this.$blocks(false) * (identationSize ?? 0), identationChar ?? "" ), }; } $seletion(input: string): CursorSelection[] { if (!this._selector) return [ { params: [], content: "", pos: input.length, }, ]; return Array.from(input.matchAll(this._selector)).map((match) => ({ pos: match.index, content: match[0], params: match.slice(1), })); } move(amount: number) { this._move = amount; return this; } block(start: string, end: string) { this._block = [start, end]; return this; } template(template: string) { this._template = template; return this; } clean(clearSelections: boolean = true) { this._clearSelections = clearSelections; return this; } prefix(prefix: string) { this._prefix = prefix; return this; } suffix(suffix: string) { this._suffix = suffix; return this; } join(str: string) { this._eachJoin = str; return this; } select(selector: RegExp) { this._selector = selector; return this; } noIdent() { this._identationChar = ""; this._identationSize = 0; return this; } ident({ size: identation, char }: { size?: number; char?: string }) { if (identation) this._identationSize = identation; if (char) this._identationChar = char; return this; } parent(parent: UCursor<{ [key: string]: Type }>) { this._parent = parent; return this; } empty(write: () => string) { const key = "_cursorWithEmptyData"; const nestedCursor = new UCursor<any>(); nestedCursor.parent(this as any); this._children.push({ key, cursor: nestedCursor, each: false }); nestedCursor._transformers.push({ key: "_cursorWithEmptyData", transformer: write, each: false, }); return this; } linebreak(amount: number = 1) { return this.empty(() => "".padStart(amount, "\n")); } expand(expanded: (cursor: UCursor<Type>) => void) { const expandedCursor = new UCursor<Type>(); expandedCursor.parent(this as any); this._children.push({ key: "_expandedCursor", cursor: expandedCursor as any, each: false, }); expanded(expandedCursor); return this; } in<Key extends keyof Type>( key: Key, nested: (cursor: UCursor<Required<Type>[Key]>) => void ) { const nestedCursor = new UCursor<Type[Key]>(); nestedCursor.parent(this as any); this._children.push({ key, cursor: nestedCursor as any, each: false }); nested(nestedCursor); return this; } each<Key extends keyof Type>( key: Key, nested: (cursor: UCursor<Required<Type>[Key][number]>) => void ) { const nestedCursor = new UCursor<Type[Key][number]>(); nestedCursor.parent(this as any); this._children.push({ key, cursor: nestedCursor as any, each: true }); nested(nestedCursor); return this; } writeFrom<Key extends keyof Type>( key: Key, transformer: CursorTransformer<Type, Key> ) { this._transformers.push({ key, transformer, each: false }); return this; } writeFromEach<Key extends keyof Type>( key: Key, transformer: CursorTransformer<Type[Key], number> ) { this._transformers.push({ key, transformer: transformer as any, each: true, }); return this; } write(transformer: CursorTransformer<{ root: Type }, "root">) { this._transformers.push({ key: "_cursorRoot", transformer: transformer as any, each: false, }); return this; } render(data: Type, input?: string, meta?: { noFix: boolean }) { input = input ?? this._template; let output = input; const selections = this.$seletion(input); const identation = this.$identation(); let transformers: { key: keyof Type; each: boolean; transformer: CursorTransformer<any, any>; }[] = []; const lastLineIsEmpty = (str: string) => str.split("\n").slice(-1)[0].length == 0; const applyIdentation = (str: string, identation: string) => str .split("\n") .map((l) => (!!l ? identation + l : l)) .join("\n"); transformers.push(...this._transformers); for (const { key, each, transformer } of transformers) { if (!transformer) continue; const transformSelections = ( transformerData: any, transformerMeta: CursorTransformerMeta ) => { for (const selection of selections) { if (selection.ignore) continue; let transformed = (lastLineIsEmpty(output) ? identation.root : "") + transformer(transformerData as any, selection, transformerMeta); const selectionInTransform = this._selector ? Array.from(transformed.matchAll(this._selector))[0] ?? null : null; const extraLength = selectionInTransform ? transformed.length - selection.content.length : transformed.length; output = output.slice(0, selection.pos) + transformed + output.slice(selection.pos + selection.content.length); selections .filter((s) => s != selection) .forEach((s) => { if (s.pos > selection.pos) s.pos += extraLength; }); if (selection.content && !selectionInTransform) selection.ignore = true; else { const addToPos = selectionInTransform ? selectionInTransform.index ?? 0 : transformed.length; selection.pos += addToPos; } } }; const root = key === "_cursorRoot"; const ignoreEmptyData = key === "_cursorWithEmptyData"; const dataSrc: any = root ? data : (data || [])[key]; if ((data === undefined || dataSrc === undefined) && !ignoreEmptyData) continue; if (each) { const total = Object.keys(dataSrc || []).length; for (const i in dataSrc) { const index = parseInt(i + ""); transformSelections(dataSrc[i], { index, total, isLast: index == total - 1, }); } } else transformSelections(dataSrc, {}); } if (this._block) output += (lastLineIsEmpty(output) ? identation.root : "") + (this._block[0] + "\n"); for (const { key, each, cursor } of this._children) { const cursorIdentation = cursor.$identation(); let cursorOutput = applyIdentation(cursor._prefix, cursorIdentation.root); if (each) { const total = Object.keys(data[key] || []).length; for (const i in data[key]) { const index = parseInt(i + ""); cursorOutput = cursor.render(data[key][i], cursorOutput, { noFix: true, }); if (total > 1 && index < total - 1) cursorOutput += cursor._eachJoin; } } else { cursorOutput = cursor.render( key == "_expandedCursor" ? data : data[key], cursorOutput, { noFix: true } ); } if (cursor._suffix) { const identedSuffix = applyIdentation( cursor._suffix, cursorIdentation.root ); cursorOutput += identedSuffix.slice( identedSuffix[0] == "\n" || lastLineIsEmpty(cursorOutput) ? 0 : cursorIdentation.root.length ); } output += cursorOutput; } if (this._block) output += "\n" + identation.root + this._block[1]; if (!meta?.noFix) output = this._prefix + output + this._suffix; if (!this.$parent()) { output = this._clean(output); } return output; } _clean(output: string) { if (this._clearSelections && this._selector) output = output.replace(this._selector, ""); this._children.forEach((child) => { output = child.cursor._clean(output); }); return output; } }