Monorepo with yarn workspaces requires yarn 3.2

This commit is contained in:
Zef Hemel
2022-04-21 13:57:45 +02:00
parent 32f3501773
commit 1f842ec1d6
167 changed files with 10424 additions and 8263 deletions
-7
View File
@@ -1,7 +0,0 @@
import { getCursor, insertAtCursor, moveCursor } from "plugos-silverbullet-syscall/editor";
export async function insertQuery() {
let cursorPos = await getCursor();
await insertAtCursor(`<!-- #query -->\n\n<!-- #end -->`);
await moveCursor(cursorPos + 12);
}
-93
View File
@@ -1,93 +0,0 @@
// Index key space:
// data:page@pos
import { IndexTreeEvent } from "../../webapp/app_event";
import { batchSet, scanPrefixGlobal } from "plugos-silverbullet-syscall";
import { collectNodesOfType, findNodeOfType, ParseTree, replaceNodesMatching } from "../../common/tree";
import { parse as parseYaml, parseAllDocuments } from "yaml";
import type { QueryProviderEvent } from "./engine";
import { applyQuery } from "./engine";
import { jsonToMDTable, removeQueries } from "./util";
export async function indexData({ name, tree }: IndexTreeEvent) {
let dataObjects: { key: string; value: Object }[] = [];
removeQueries(tree);
collectNodesOfType(tree, "FencedCode").forEach((t) => {
let codeInfoNode = findNodeOfType(t, "CodeInfo");
if (!codeInfoNode) {
return;
}
if (codeInfoNode.children![0].text !== "data") {
return;
}
let codeTextNode = findNodeOfType(t, "CodeText");
if (!codeTextNode) {
// Honestly, this shouldn't happen
return;
}
let codeText = codeTextNode.children![0].text!;
try {
// We support multiple YAML documents in one block
for (let doc of parseAllDocuments(codeText)) {
if (!doc.contents) {
continue;
}
console.log(doc.contents.toJSON());
dataObjects.push({
key: `data:${name}@${t.from! + doc.range[0]}`,
value: doc.contents.toJSON(),
});
}
// console.log("Parsed data", parsedData);
} catch (e) {
console.error("Could not parse data", codeText, "error:", e);
return;
}
});
console.log("Found", dataObjects.length, "data objects");
await batchSet(name, dataObjects);
}
export function extractMeta(parseTree: ParseTree, remove = false): any {
let data = {};
replaceNodesMatching(parseTree, (t) => {
if (t.type !== "FencedCode") {
return;
}
let codeInfoNode = findNodeOfType(t, "CodeInfo");
if (!codeInfoNode) {
return;
}
if (codeInfoNode.children![0].text !== "meta") {
return;
}
let codeTextNode = findNodeOfType(t, "CodeText");
if (!codeTextNode) {
// Honestly, this shouldn't happen
return;
}
let codeText = codeTextNode.children![0].text!;
data = parseYaml(codeText);
return remove ? null : undefined;
});
return data;
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<string> {
let allData: any[] = [];
for (let { key, page, value } of await scanPrefixGlobal("data:")) {
let [, pos] = key.split("@");
allData.push({
...value,
page: page,
pos: +pos,
});
}
let resultData = applyQuery(query, allData);
return jsonToMDTable(resultData);
}
-95
View File
@@ -1,95 +0,0 @@
import { expect, test } from "@jest/globals";
import { applyQuery, parseQuery } from "./engine";
test("Test parser", () => {
let parsedBasicQuery = parseQuery(`page`);
expect(parsedBasicQuery.table).toBe("page");
let parsedQuery1 = parseQuery(
`task where completed = false and dueDate <= "{{today}}" order by dueDate desc limit 5`
);
expect(parsedQuery1.table).toBe("task");
expect(parsedQuery1.orderBy).toBe("dueDate");
expect(parsedQuery1.orderDesc).toBe(true);
expect(parsedQuery1.limit).toBe(5);
expect(parsedQuery1.filter.length).toBe(2);
expect(parsedQuery1.filter[0]).toStrictEqual({
op: "=",
prop: "completed",
value: false,
});
expect(parsedQuery1.filter[1]).toStrictEqual({
op: "<=",
prop: "dueDate",
value: "{{today}}",
});
let parsedQuery2 = parseQuery(`page where name =~ /interview\\/.*/"`);
expect(parsedQuery2.table).toBe("page");
expect(parsedQuery2.filter.length).toBe(1);
expect(parsedQuery2.filter[0]).toStrictEqual({
op: "=~",
prop: "name",
value: "interview\\/.*",
});
let parsedQuery3 = parseQuery(`page where something != null`);
expect(parsedQuery3.table).toBe("page");
expect(parsedQuery3.filter.length).toBe(1);
expect(parsedQuery3.filter[0]).toStrictEqual({
op: "!=",
prop: "something",
value: null,
});
expect(parseQuery(`page select name`).select).toStrictEqual(["name"]);
expect(parseQuery(`page select name, age`).select).toStrictEqual([
"name",
"age",
]);
});
test("Test performing the queries", () => {
let data: any[] = [
{ name: "interview/My Interview", lastModified: 1 },
{ name: "interview/My Interview 2", lastModified: 2 },
{ name: "Pete", age: 38 },
{ name: "Angie", age: 28 },
];
expect(
applyQuery(parseQuery(`page where name =~ /interview\\/.*/`), data)
).toStrictEqual([
{ name: "interview/My Interview", lastModified: 1 },
{ name: "interview/My Interview 2", lastModified: 2 },
]);
expect(
applyQuery(
parseQuery(`page where name =~ /interview\\/.*/ order by lastModified`),
data
)
).toStrictEqual([
{ name: "interview/My Interview", lastModified: 1 },
{ name: "interview/My Interview 2", lastModified: 2 },
]);
expect(
applyQuery(
parseQuery(
`page where name =~ /interview\\/.*/ order by lastModified desc`
),
data
)
).toStrictEqual([
{ name: "interview/My Interview 2", lastModified: 2 },
{ name: "interview/My Interview", lastModified: 1 },
]);
expect(applyQuery(parseQuery(`page where age > 30`), data)).toStrictEqual([
{ name: "Pete", age: 38 },
]);
expect(
applyQuery(parseQuery(`page where age > 28 and age < 38`), data)
).toStrictEqual([]);
expect(
applyQuery(parseQuery(`page where age > 30 select name`), data)
).toStrictEqual([{ name: "Pete" }]);
});
-193
View File
@@ -1,193 +0,0 @@
import { collectNodesOfType, findNodeOfType, replaceNodesMatching } from "../../common/tree";
import { lezerToParseTree } from "../../common/parse_tree";
// @ts-ignore
import { parser } from "./parse-query";
export type QueryProviderEvent = {
query: ParsedQuery;
pageName: string;
};
export type Filter = {
op: string;
prop: string;
value: any;
};
export type ParsedQuery = {
table: string;
orderBy?: string;
orderDesc?: boolean;
limit?: number;
filter: Filter[];
select?: string[];
};
export function parseQuery(query: string): ParsedQuery {
let n = lezerToParseTree(query, parser.parse(query).topNode);
// Clean the tree a bit
replaceNodesMatching(n, (n) => {
if (!n.type) {
let trimmed = n.text!.trim();
if (!trimmed) {
return null;
}
n.text = trimmed;
}
});
// console.log("Parsed", JSON.stringify(n, null, 2));
let queryNode = n.children![0];
let parsedQuery: ParsedQuery = {
table: queryNode.children![0].children![0].text!,
filter: [],
};
let orderByNode = findNodeOfType(queryNode, "OrderClause");
if (orderByNode) {
let nameNode = findNodeOfType(orderByNode, "Name");
parsedQuery.orderBy = nameNode!.children![0].text!;
let orderNode = findNodeOfType(orderByNode, "Order");
parsedQuery.orderDesc = orderNode
? orderNode.children![0].text! === "desc"
: false;
}
let limitNode = findNodeOfType(queryNode, "LimitClause");
if (limitNode) {
let nameNode = findNodeOfType(limitNode, "Number");
parsedQuery.limit = +nameNode!.children![0].text!;
}
let filterNodes = collectNodesOfType(queryNode, "FilterExpr");
for (let filterNode of filterNodes) {
let val: any = undefined;
let valNode = filterNode.children![2].children![0];
switch (valNode.type) {
case "Number":
val = valNode.children![0].text!;
break;
case "Bool":
val = valNode.children![0].text! === "true";
break;
case "Null":
val = null;
break;
case "Name":
val = valNode.children![0].text!;
break;
case "Regex":
val = valNode.children![0].text!;
val = val.substring(1, val.length - 1);
break;
case "String":
val = valNode.children![0].text!;
val = val.substring(1, val.length - 1);
break;
}
let f: Filter = {
prop: filterNode.children![0].children![0].text!,
op: filterNode.children![1].text!,
value: val,
};
parsedQuery.filter.push(f);
}
let selectNode = findNodeOfType(queryNode, "SelectClause");
if (selectNode) {
console.log("Select node", JSON.stringify(selectNode));
parsedQuery.select = [];
collectNodesOfType(selectNode, "Name").forEach((t) => {
parsedQuery.select!.push(t.children![0].text!);
});
// let nameNode = findNodeOfType(selectNode, "Number");
// parsedQuery.limit = +nameNode!.children![0].text!;
}
// console.log(JSON.stringify(queryNode, null, 2));
return parsedQuery;
}
export function applyQuery<T>(parsedQuery: ParsedQuery, records: T[]): T[] {
let resultRecords: any[] = [];
if (parsedQuery.filter.length === 0) {
resultRecords = records.slice();
} else {
recordLoop: for (let record of records) {
const recordAny: any = record;
for (let { op, prop, value } of parsedQuery.filter) {
switch (op) {
case "=":
if (!(recordAny[prop] == value)) {
continue recordLoop;
}
break;
case "!=":
if (!(recordAny[prop] != value)) {
continue recordLoop;
}
break;
case "<":
if (!(recordAny[prop] < value)) {
continue recordLoop;
}
break;
case "<=":
if (!(recordAny[prop] <= value)) {
continue recordLoop;
}
break;
case ">":
if (!(recordAny[prop] > value)) {
continue recordLoop;
}
break;
case ">=":
if (!(recordAny[prop] >= value)) {
continue recordLoop;
}
break;
case "=~":
// TODO: Cache regexps somehow
if (!new RegExp(value).exec(recordAny[prop])) {
continue recordLoop;
}
break;
case "!=~":
if (new RegExp(value).exec(recordAny[prop])) {
continue recordLoop;
}
break;
}
}
resultRecords.push(recordAny);
}
}
// Now the sorting
if (parsedQuery.orderBy) {
resultRecords = resultRecords.sort((a: any, b: any) => {
const orderBy = parsedQuery.orderBy!;
const orderDesc = parsedQuery.orderDesc!;
if (a[orderBy] === b[orderBy]) {
return 0;
}
if (a[orderBy] < b[orderBy]) {
return orderDesc ? 1 : -1;
} else {
return orderDesc ? -1 : 1;
}
});
}
if (parsedQuery.limit) {
resultRecords = resultRecords.slice(0, parsedQuery.limit);
}
if (parsedQuery.select) {
resultRecords = resultRecords.map((rec) => {
let newRec: any = {};
for (let k of parsedQuery.select!) {
newRec[k] = rec[k];
}
return newRec;
});
}
return resultRecords;
}
-60
View File
@@ -1,60 +0,0 @@
import { flashNotification, getCurrentPage, getText, reloadPage, save } from "plugos-silverbullet-syscall/editor";
import { readPage, writePage } from "plugos-silverbullet-syscall/space";
import { invokeFunction } from "plugos-silverbullet-syscall/system";
import { parseQuery } from "./engine";
import { replaceTemplateVars } from "../core/template";
import { queryRegex, removeQueries } from "./util";
import { dispatch } from "plugos-syscall/event";
import { replaceAsync } from "../lib/util";
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
export async function updateMaterializedQueriesCommand() {
const currentPage = await getCurrentPage();
await save();
await flashNotification("Updating materialized queries...");
await invokeFunction(
"server",
"updateMaterializedQueriesOnPage",
currentPage
);
await reloadPage();
await flashNotification("Updated materialized queries");
}
export async function whiteOutQueriesCommand() {
const text = await getText();
const parsed = await parseMarkdown(text);
console.log(removeQueries(parsed));
}
// Called from client, running on server
export async function updateMaterializedQueriesOnPage(pageName: string) {
let { text } = await readPage(pageName);
text = await replaceAsync(
text,
queryRegex,
async (fullMatch, startQuery, query, body, endQuery) => {
let parsedQuery = parseQuery(replaceTemplateVars(query, pageName));
console.log("Parsed query", parsedQuery);
// Let's dispatch an event and see what happens
let results = await dispatch(
`query:${parsedQuery.table}`,
{ query: parsedQuery, pageName: pageName },
10 * 1000
);
if (results.length === 0) {
return `${startQuery}\n${endQuery}`;
} else if (results.length === 1) {
return `${startQuery}\n${results[0]}\n${endQuery}`;
} else {
console.error("Too many query results", results);
return fullMatch;
}
}
);
// console.log("New text", text);
await writePage(pageName, text);
}
-17
View File
@@ -1,17 +0,0 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import { LRParser } from "@lezer/lr";
export const parser = LRParser.deserialize({
version: 13,
states: "%WOVQPOOO[QQO'#C^QOQPOOOmQPO'#C`OrQQO'#CjOwQPO'#ClO|QPO'#CmOOQO'#Cn'#CnO!RQQO,58xO!dQPO'#CcO#OQQO'#CaOOQO'#Ca'#CaOOQO,58z,58zO#dQPO,59UOOQO,59W,59WO#iQQO'#DWOOQO,59X,59XOOQO-E6l-E6lO#}QQO,58}OmQPO,58|O$cQQO1G.pO$zQPO'#CoO%PQQO,59rOOQO'#Cg'#CgOOQO'#Ci'#CiOOQO'#Cd'#CdOOQO1G.i1G.iOOQO1G.h1G.hOOQO'#Ck'#CkOOQO7+$[7+$[OOQO,59Z,59ZOOQO-E6m-E6m",
stateData: "%e~OfOS~ORPO~OgROtSOxTOyUOdQX~ORXO~Ou]O~OX^O~OR_O~OgROtSOxTOyUOdQa~OhbOlbOmbOnbOobOpbOqbOrbO~OscOdTXgTXtTXxTXyTX~ORdO~O{eOdzXgzXtzXxzXyzX~OXiOYiO[iOigOjgOkhO~OvlOwlOd^ig^it^ix^iy^i~ORnO~O{eOdzagzatzaxzayza~O",
goto: "!y{PP|P!P!T!W!Z!aPP!dP!d!P!g!P!P!j!pPPPPPPPPPPPPPPPPPPPPPP!vRQOTVPWR[RRZRQYRRkcRjbRibRmdQWPRaWQf_RofR`U",
nodeNames: "⚠ Program Query Name WhereClause LogicalExpr AndExpr FilterExpr Value Number String Bool Regex Null OrderClause Order LimitClause SelectClause",
maxTerm: 43,
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "=_~RxX^#opq#oqr$drs$w|}%c}!O%h!P!Q%y!Q![&p!^!_&x!_!`'V!`!a'd!c!}%h#R#S%h#T#U'q#U#V*W#V#W%h#W#X+S#X#Y%h#Y#Z-O#Z#`%h#`#a/`#a#b%h#b#c1s#c#d3o#d#g%h#g#h6S#h#i9O#i#k%h#k#l:z#l#o%h#y#z#o$f$g#o#BY#BZ#o$IS$I_#o$Ip$Iq$w$Iq$Ir$w$I|$JO#o$JT$JU#o$KV$KW#o&FU&FV#o~#tYf~X^#opq#o#y#z#o$f$g#o#BY#BZ#o$IS$I_#o$I|$JO#o$JT$JU#o$KV$KW#o&FU&FV#o~$gP!_!`$j~$oPn~#r#s$r~$wOr~~$zUOr$wrs%^s$Ip$w$Ip$Iq%^$Iq$Ir%^$Ir~$w~%cOY~~%hO{~P%mSRP}!O%h!c!}%h#R#S%h#T#o%h~&OV[~OY%yZ]%y^!P%y!P!Q&e!Q#O%y#O#P&j#P~%y~&jO[~~&mPO~%y~&uPX~!Q![&p~&}Ph~!_!`'Q~'VOl~~'[Pm~#r#s'_~'dOq~~'iPp~!_!`'l~'qOo~R'vWRP}!O%h!c!}%h#R#S%h#T#b%h#b#c(`#c#g%h#g#h)[#h#o%hR(eURP}!O%h!c!}%h#R#S%h#T#W%h#W#X(w#X#o%hR)OSsQRP}!O%h!c!}%h#R#S%h#T#o%hR)aURP}!O%h!c!}%h#R#S%h#T#V%h#V#W)s#W#o%hR)zSwQRP}!O%h!c!}%h#R#S%h#T#o%hR*]URP}!O%h!c!}%h#R#S%h#T#m%h#m#n*o#n#o%hR*vSuQRP}!O%h!c!}%h#R#S%h#T#o%hR+XURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y+k#Y#o%hR+pURP}!O%h!c!}%h#R#S%h#T#g%h#g#h,S#h#o%hR,XURP}!O%h!c!}%h#R#S%h#T#V%h#V#W,k#W#o%hR,rSvQRP}!O%h!c!}%h#R#S%h#T#o%hR-TTRP}!O%h!c!}%h#R#S%h#T#U-d#U#o%hR-iURP}!O%h!c!}%h#R#S%h#T#`%h#`#a-{#a#o%hR.QURP}!O%h!c!}%h#R#S%h#T#g%h#g#h.d#h#o%hR.iURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y.{#Y#o%hR/SSjQRP}!O%h!c!}%h#R#S%h#T#o%hR/eURP}!O%h!c!}%h#R#S%h#T#]%h#]#^/w#^#o%hR/|URP}!O%h!c!}%h#R#S%h#T#a%h#a#b0`#b#o%hR0eURP}!O%h!c!}%h#R#S%h#T#]%h#]#^0w#^#o%hR0|URP}!O%h!c!}%h#R#S%h#T#h%h#h#i1`#i#o%hR1gSxQRP}!O%h!c!}%h#R#S%h#T#o%hR1xURP}!O%h!c!}%h#R#S%h#T#i%h#i#j2[#j#o%hR2aURP}!O%h!c!}%h#R#S%h#T#`%h#`#a2s#a#o%hR2xURP}!O%h!c!}%h#R#S%h#T#`%h#`#a3[#a#o%hR3cSkQRP}!O%h!c!}%h#R#S%h#T#o%hR3tURP}!O%h!c!}%h#R#S%h#T#f%h#f#g4W#g#o%hR4]URP}!O%h!c!}%h#R#S%h#T#W%h#W#X4o#X#o%hR4tURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y5W#Y#o%hR5]URP}!O%h!c!}%h#R#S%h#T#f%h#f#g5o#g#o%hR5vStQRP}!O%h!c!}%h#R#S%h#T#o%hR6XURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y6k#Y#o%hR6pURP}!O%h!c!}%h#R#S%h#T#`%h#`#a7S#a#o%hR7XURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y7k#Y#o%hR7pURP}!O%h!c!}%h#R#S%h#T#V%h#V#W8S#W#o%hR8XURP}!O%h!c!}%h#R#S%h#T#h%h#h#i8k#i#o%hR8rSyQRP}!O%h!c!}%h#R#S%h#T#o%hR9TURP}!O%h!c!}%h#R#S%h#T#f%h#f#g9g#g#o%hR9lURP}!O%h!c!}%h#R#S%h#T#i%h#i#j:O#j#o%hR:TURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y:g#Y#o%hR:nSiQRP}!O%h!c!}%h#R#S%h#T#o%hR;PURP}!O%h!c!}%h#R#S%h#T#[%h#[#];c#]#o%hR;hURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y;z#Y#o%hR<PURP}!O%h!c!}%h#R#S%h#T#f%h#f#g<c#g#o%hR<hURP}!O%h!c!}%h#R#S%h#T#X%h#X#Y<z#Y#o%hR=RSgQRP}!O%h!c!}%h#R#S%h#T#o%h",
tokenizers: [0, 1],
topRules: {"Program":[0,1]},
tokenPrec: 0
})
-19
View File
@@ -1,19 +0,0 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Program = 1,
Query = 2,
Name = 3,
WhereClause = 4,
LogicalExpr = 5,
AndExpr = 6,
FilterExpr = 7,
Value = 8,
Number = 9,
String = 10,
Bool = 11,
Regex = 12,
Null = 13,
OrderClause = 14,
Order = 15,
LimitClause = 16,
SelectClause = 17
-58
View File
@@ -1,58 +0,0 @@
@precedence { logic @left }
@top Program { Query }
Query {
Name ( WhereClause | OrderClause | LimitClause | SelectClause )*
}
commaSep<content> { content ("," content)* }
WhereClause { "where" LogicalExpr }
OrderClause { "order" "by" Name Order? }
LimitClause { "limit" Number }
SelectClause { "select" commaSep<Name> }
Order {
"desc" | "asc"
}
Value { Number | String | Bool | Regex | Null }
LogicalExpr { AndExpr | FilterExpr }
AndExpr { FilterExpr !logic "and" FilterExpr }
FilterExpr {
Name "<" Value
| Name "<=" Value
| Name "=" Value
| Name "!=" Value
| Name ">=" Value
| Name ">" Value
| Name "=~" Value
| Name "!=~" Value
}
@skip { space }
Bool {
"true" | "false"
}
Null {
"null"
}
@tokens {
space { std.whitespace+ }
Name { (std.asciiLetter | "-" | "_")+ }
String {
("\"" | "“" | "”") ![\"”“]* ("\"" | "“" | "”")
}
Regex { "/" ( ![/\\\n\r] | "\\" _ )* "/"? }
Number { std.digit+ }
}
-24
View File
@@ -1,24 +0,0 @@
functions:
updateMaterializedQueriesOnPage:
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage
updateMaterializedQueriesCommand:
path: ./materialized_queries.ts:updateMaterializedQueriesCommand
command:
name: "Materialized Queries: Update"
key: "Alt-q"
whiteOutQueriesCommand:
path: ./materialized_queries.ts:whiteOutQueriesCommand
command:
name: "Debug: Whiteout Queries"
indexData:
path: ./data.ts:indexData
events:
- page:index
dataQueryProvider:
path: ./data.ts:queryProvider
events:
- query:data
insertQueryCommand:
path: ./command.ts:insertQuery
slashCommand:
name: query
-70
View File
@@ -1,70 +0,0 @@
import { addParentPointers, collectNodesMatching, ParseTree, renderToText } from "../../common/tree";
export const queryRegex =
/(<!--\s*#query\s+(.+?)-->)(.+?)(<!--\s*#end\s*-->)/gs;
export const queryStartRegex = /<!--\s*#query\s+(.+?)-->/s;
export const queryEndRegex = /<!--\s*#end\s*-->/s;
// export function whiteOutQueries(text: string): string {
// return text.replaceAll(queryRegex, (match) =>
// new Array(match.length + 1).join(" ")
// );
// }
export function removeQueries(pt: ParseTree) {
addParentPointers(pt);
collectNodesMatching(pt, (t) => {
if (t.type !== "CommentBlock") {
return false;
}
let text = t.children![0].text!;
if (!queryStartRegex.exec(text)) {
return false;
}
let parentChildren = t.parent!.children!;
let index = parentChildren.indexOf(t);
let nodesToReplace: ParseTree[] = [];
for (let i = index + 1; i < parentChildren.length; i++) {
let n = parentChildren[i];
if (n.type === "CommentBlock") {
let text = n.children![0].text!;
if (queryEndRegex.exec(text)) {
break;
}
}
nodesToReplace.push(n);
}
let renderedText = nodesToReplace.map(renderToText).join("");
parentChildren.splice(index + 1, nodesToReplace.length, {
text: new Array(renderedText.length + 1).join(" "),
});
return true;
});
}
// Nicely format an array of JSON objects as a Markdown table
export function jsonToMDTable(
jsonArray: any[],
valueTransformer?: (k: string, v: any) => string | undefined
): string {
let headers = new Set<string>();
for (let entry of jsonArray) {
for (let k of Object.keys(entry)) {
headers.add(k);
}
}
let headerList = [...headers];
let lines = [];
lines.push("|" + headerList.join("|") + "|");
lines.push("|" + headerList.map((title) => "----").join("|") + "|");
for (const val of jsonArray) {
let el = [];
for (let prop of headerList) {
el.push(valueTransformer ? valueTransformer(prop, val[prop]) : val[prop]);
}
lines.push("|" + el.join("|") + "|");
}
return lines.join("\n");
}