@udraft/cursor
Version:
uDraft Cursor enables you to declarative write code generators!
382 lines (327 loc) • 9.87 kB
text/typescript
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;
}
}