Page MenuHomeSealhub

No OneTemporary

diff --git a/src/component-arguments/table.ts b/src/component-arguments/table.ts
index 9c4e78c..69502e7 100644
--- a/src/component-arguments/table.ts
+++ b/src/component-arguments/table.ts
@@ -1,226 +1,225 @@
import { hasShape, predicates } from "@sealcode/ts-predicates";
import { JDDContext } from "../index.js";
import { ComponentArgument } from "./component-argument.js";
import { ContainerArgument } from "./container-argument.js";
type TableHeader<HeaderType> = { type: "header"; header_content: HeaderType };
type TableRegularRow<CellType> = { type: "row"; cells: CellType[] };
export type TableRow<CellType, HeaderType> =
| TableHeader<HeaderType>
| TableRegularRow<CellType>;
export type TableData<CellType, HeaderType> = {
rows: TableRow<CellType, HeaderType>[];
};
export function isTableHeader(x: unknown): x is TableHeader<unknown> {
return hasShape(
{
type: predicates.const("header"),
header_content: predicates.unknown,
},
x
);
}
export function isTableRegularRow<CellType = unknown>(
x: unknown
): x is TableRegularRow<CellType> {
return hasShape(
{
type: predicates.const("row"),
cells: predicates.array(predicates.unknown),
},
x
);
}
export function isTableData<CellType = unknown, HeaderType = unknown>(
x: unknown
): x is TableData<CellType, HeaderType> {
return hasShape(
{
rows: predicates.array(predicates.object),
},
x
);
}
export class Table<CellType, HeaderType> extends ContainerArgument<
TableData<CellType, HeaderType>
> {
constructor(
public header_type: ComponentArgument<HeaderType, unknown, unknown>,
public cell_type: ComponentArgument<CellType, unknown, unknown>
) {
super();
cell_type.parent_argument = this;
header_type.parent_argument = this;
}
getSubArgument(
[_, key, ...rest]: string[],
value: TableData<CellType, HeaderType>
) {
- console.log("table.ts:66", { key, rest, value });
if (isNaN(parseInt(key))) {
return <const>[null, [] as string[], null];
}
const key_n = parseInt(key);
const row = value.rows[key_n];
if (!row) {
return <const>[null, [] as string[], null];
} else if (row.type == "header") {
return <const>[this.header_type, rest, row.header_content];
} else {
let cell_index = rest.shift();
if (cell_index == "cells") {
cell_index = rest.shift();
}
if (!cell_index) {
throw new Error("Missing cell index");
}
const parsed_cell_index = parseInt(cell_index);
if (isNaN(parsed_cell_index)) {
throw new Error(
`Cell index should be a number, got: ${cell_index}`
);
}
return <const>[this.cell_type, rest, row.cells[parsed_cell_index]];
}
}
getTypeName() {
return "table";
}
getEmptyValue() {
return {
rows: [
{
type: <const>"header",
header_content: this.header_type.getEmptyValue(),
},
{
type: <const>"row",
cells: [this.cell_type.getEmptyValue()],
},
],
};
}
async getExampleValue(context: JDDContext) {
const rows = Math.round(Math.random() * 5);
const columns = Math.round(Math.random() * 5);
const result: TableData<CellType, HeaderType> = {
rows: [
{
type: "header",
header_content: await this.header_type.getExampleValue(
context
),
},
],
};
for (let i = 0; i < rows; i++) {
const cells: CellType[] = [];
for (let j = 0; j < columns; j++) {
// eslint-disable-next-line no-await-in-loop
cells.push(await this.cell_type.getExampleValue(context));
}
result.rows.push({ type: "row", cells });
}
return result;
}
countWords(value: TableData<CellType, HeaderType>): number {
let result = 0;
for (let i = 0; i < value.rows.length; i++) {
const row = value.rows[i];
if (isTableHeader(row)) {
result += this.header_type.countWords(row.header_content);
} else {
for (let j = 0; j < row.cells.length; j++) {
result += this.cell_type.countWords(row.cells[j]);
}
}
}
return result;
}
async processAllSubarguments(
context: JDDContext,
input: unknown,
processing_function: (
argument: ComponentArgument<unknown>,
value: unknown
) => Promise<unknown>
) {
if (!hasShape({ rows: predicates.array(predicates.object) }, input)) {
return { rows: [] };
} else {
const result: TableData<CellType, HeaderType> = { rows: [] };
const row_promises = input.rows.map(async (row, row_index) => {
let new_row: TableRow<CellType, HeaderType>;
if (hasShape({ header_content: predicates.unknown }, row)) {
let header_content = (await processing_function(
this.header_type,
row.header_content
)) as HeaderType | HeaderType[] | null;
if (Array.isArray(header_content)) {
header_content = header_content[0];
}
if (header_content == null) {
header_content = this.header_type.getEmptyValue();
}
new_row = {
type: "header",
header_content,
};
result.rows[row_index] = new_row;
} else if (
hasShape(
{ cells: predicates.array(predicates.unknown) },
row
)
) {
new_row = {
type: "row",
cells: await Promise.all(
row.cells.map(async (cell) => {
const value = (await processing_function(
this.cell_type,
cell
)) as CellType | CellType[] | null;
if (value === null) {
return this.cell_type.getEmptyValue();
} else if (Array.isArray(value)) {
return value[0];
} else {
return value;
}
})
),
};
result.rows[row_index] = new_row;
}
});
await Promise.all(row_promises);
return result;
}
}
getColumnsCount(value: TableData<CellType, HeaderType>) {
return (
(
(
value.rows.filter((row) => row.type == "row")[0] as
| TableRegularRow<CellType>
| undefined
)?.cells || []
).length || 1
);
}
}
diff --git a/src/component.test.ts b/src/component.test.ts
index 79ef26a..f8cf150 100644
--- a/src/component.test.ts
+++ b/src/component.test.ts
@@ -1,50 +1,49 @@
import assert from "assert";
import { Table } from "./component-arguments/table.js";
import { Component } from "./component.js";
import { List, ShortText, Structured } from "./index.js";
describe("component class", () => {
describe("getSubArgument", () => {
it("traverses argument path that includes a table", () => {
const args = {
table: new Table(
new Structured({ title: new ShortText() }),
new Structured({ tags: new List(new ShortText()) })
),
};
const component = new (class extends Component<typeof args> {
getArguments() {
return args;
}
toHTML() {
return "";
}
})();
const [arg, _, values] = component.getArgumentAtPath(
"table/rows/2/cells/0/tags".split("/"),
{
table: {
rows: [
{
type: "header",
header_content: { title: "hehe" },
},
{
type: "header",
header_content: { title: "hehe2" },
},
{
type: "row",
cells: [{ tags: ["tag1", "tag2"] }],
},
],
},
}
);
- console.log(arg);
assert(arg instanceof List);
assert.deepStrictEqual(values, ["tag1", "tag2"]);
});
});
});
diff --git a/src/component.ts b/src/component.ts
index ae556e3..0450ae1 100644
--- a/src/component.ts
+++ b/src/component.ts
@@ -1,154 +1,152 @@
import { FlatTemplatable } from "tempstream";
import {
ComponentArgument,
ExtractStructuredComponentArgumentsParsed,
ExtractStructuredComponentArgumentsReceived,
ExtractStructuredComponentArgumentsStorage,
JDDContext,
} from "./index.js";
export interface ComponentConstructor<
A extends Record<string, ComponentArgument<unknown>> = Record<
string,
ComponentArgument<unknown>
>
> {
new (): Component<A>;
}
export type EarlyAsset = (
| { type: "script" | "style"; url: string; integrity?: string }
| { type: "script" | "style"; content: string }
) & { identity: string }; // identity key will be used for deduplication
export abstract class Component<
ArgumentsT extends Record<string, ComponentArgument<unknown>> = Record<
string,
ComponentArgument<unknown>
>
> {
abstract getArguments(): ArgumentsT;
abstract toHTML(
args: ExtractStructuredComponentArgumentsParsed<ArgumentsT>,
context: JDDContext
): FlatTemplatable | Promise<FlatTemplatable>;
async getEarlyAssets(
_args: ExtractStructuredComponentArgumentsParsed<ArgumentsT>,
_context: JDDContext
): Promise<EarlyAsset[]> {
return [];
}
countWords(args: Record<string, unknown>): number {
return Object.entries(args).reduce((acc, [arg_name, value]) => {
const arg = this.getArguments()[arg_name];
if (!arg) {
console.warn(
`Arguemnt ${arg_name} was not found in the component`
);
return acc + 0;
}
return acc + arg.countWords(value);
}, 0);
}
async getExampleValues(
context: JDDContext
): Promise<ExtractStructuredComponentArgumentsParsed<ArgumentsT>> {
return Object.fromEntries(
await Promise.all(
Object.entries(this.getArguments()).map(
async ([key, value]) => [
key,
await value.getExampleValue(context),
]
)
)
) as ExtractStructuredComponentArgumentsParsed<ArgumentsT>;
}
async convertReceivedValuesToParsed(
context: JDDContext,
values: ExtractStructuredComponentArgumentsReceived<ArgumentsT>
): Promise<ExtractStructuredComponentArgumentsParsed<ArgumentsT>> {
const args = this.getArguments();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.fromEntries(
await Promise.all(
Object.entries(values).map(async ([key, value]) => {
return [
key,
await args[key].receivedToParsed(context, value),
];
})
)
);
}
async convertParsedToStorage(
context: JDDContext,
values: ExtractStructuredComponentArgumentsParsed<ArgumentsT>
): Promise<ExtractStructuredComponentArgumentsStorage<ArgumentsT>> {
const args = this.getArguments();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.fromEntries(
await Promise.all(
Object.entries(values).map(async ([key, value]) => {
return [
key,
await args[key].parsedToStorage(context, value),
];
})
)
);
}
async convertStorageToParsed(
context: JDDContext,
values: ExtractStructuredComponentArgumentsStorage<ArgumentsT>
): Promise<ExtractStructuredComponentArgumentsParsed<ArgumentsT>> {
const args = this.getArguments();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.fromEntries(
await Promise.all(
Object.entries(values).map(async ([key, value]) => {
return [
key,
await args[key].storageToParsed(context, value),
];
})
)
);
}
// returns the argument, remaining path, and the values for that argument
getArgumentAtPath(
argument_path: string[],
values: ExtractStructuredComponentArgumentsParsed<ArgumentsT>
): [ComponentArgument<unknown> | null, string[], unknown] {
- console.log("component.ts:129", argument_path);
argument_path = [...argument_path];
if (argument_path.length == 0) {
return [null, [], null];
}
const arg_name = argument_path.shift() as string;
let argument: ComponentArgument<unknown> | null =
this.getArguments()[arg_name];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
values = values[arg_name] as any;
if (argument_path.length == 0) {
return [argument, [], values];
}
do {
- console.log("component.ts:139", argument_path);
// the getSubArgument method can consume as many keys from the path as it wants
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
[argument, argument_path, values] = argument.getSubArgument(
argument_path,
values
) as any;
} while (argument_path.length && argument !== null);
return [argument, argument_path, values];
}
}
diff --git a/src/index.ts b/src/index.ts
index 711ce3e..d4d728f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,132 +1,153 @@
import { tempstream } from "tempstream";
import { JDDContext } from "./jdd-context.js";
import { Registry } from "./registry.js";
import { EarlyAsset } from "./component.js";
import { hasField } from "@sealcode/ts-predicates";
-import { JDDocumentContainer } from "./document.js";
+import {
+ JDDocumentContainer,
+ RawJDDocument,
+ documentContainerFromStorage,
+} from "./document.js";
+import { documentToParsed } from "./document.js";
export * from "./component.js";
export * from "./component-arguments/component-argument.js";
export * as ComponentArguments from "./component-arguments/component-arguments.js";
export * from "./component-arguments/component-arguments.js"; // exporting this also as root elements to make it easier to auto-import those
export * from "./jdd-context.js";
export * from "./registry.js";
export { insert_nbsp } from "./utils/insert_nbsp.js";
export * from "./document.js";
export function countWords(
registry: Registry,
document_container: JDDocumentContainer<"parsed">
): number {
return document_container.value.reduce((acc, { component_name, args }) => {
const component = registry.get(component_name);
if (!component) {
console.warn(
"Component not found in the registry: " + component_name
);
return acc + 0;
}
return acc + component.countWords(args);
}, 0);
}
export async function renderEarlyAssets(
registry: Registry,
document_container: JDDocumentContainer<"parsed">,
context: JDDContext
) {
const early_assets = (
await Promise.all(
document_container.value.map(async ({ component_name, args }) => {
const component = registry.get(component_name);
if (!component) {
console.warn(
"Component not found in the registry: " + component_name
);
return [];
}
for (const arg_name in component?.getArguments()) {
if (!Object.prototype.hasOwnProperty.call(args, arg_name)) {
args[arg_name] = component
?.getArguments()
[arg_name]?.getEmptyValue();
}
}
return await component.getEarlyAssets(args, context);
})
)
).flat();
const deduplicated_assets: Record<string, EarlyAsset> = {};
for (const asset of early_assets) {
deduplicated_assets[asset.identity] = asset;
}
return Object.values(deduplicated_assets)
.map((asset) => {
if (asset.type == "script") {
if (hasField("url", asset)) {
return /* HTML */ `<script
async
src="${asset.url}"
onLoad="document.dispatchEvent(new Event('loaded-${asset.identity}'))"
${(asset.integrity &&
`integrity="${asset.integrity}" crossorigin="anonymous"`) ||
""}
></script>`;
} else {
return /* HTML */ `<script><${asset.content}/script>`;
}
} else if (asset.type == "style") {
if (hasField("url", asset)) {
const integrity =
(asset.integrity &&
`integrity="${asset.integrity}" crossorigin="anonymous"`) ||
"";
// see https://web.dev/articles/defer-non-critical-css
return /* HTML */ `<link
rel="preload"
href="${asset.url}"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
${integrity}
/>
<noscript
><link
rel="stylesheet"
href="${asset.url}"
${integrity}
/></noscript>`;
} else {
return /* HTML */ `<style>
${asset.content}
</style>`;
}
}
})
.join(" ");
}
export function render(
registry: Registry,
document: JDDocumentContainer<"parsed">,
context: JDDContext
) {
return tempstream`${document.value.map(({ component_name, args }) => {
const component = registry.get(component_name);
if (!component) {
console.warn(
"Component not found in the registry: " + component_name
);
return "";
}
for (const arg_name in component?.getArguments()) {
if (!Object.prototype.hasOwnProperty.call(args, arg_name)) {
args[arg_name] = component
?.getArguments()
[arg_name]?.getEmptyValue();
}
}
return component.toHTML(args, context);
})}`;
}
+
+export async function renderFromStorage(
+ registry: Registry,
+ document: RawJDDocument,
+ jdd_context: JDDContext
+) {
+ return render(
+ registry,
+ await documentToParsed(
+ registry,
+ jdd_context,
+ documentContainerFromStorage(document)
+ ),
+ jdd_context
+ );
+}
diff --git a/src/render.test.ts b/src/render.test.ts
new file mode 100644
index 0000000..38bb49b
--- /dev/null
+++ b/src/render.test.ts
@@ -0,0 +1,30 @@
+import { streamToString } from "tempstream";
+import { makeSimpleJDDContext, renderFromStorage } from "./index.js";
+import { Registry } from "./registry.js";
+import { Markdown } from "./components/markdown.js";
+import { FileManager } from "@sealcode/file-manager";
+import assert from "assert";
+
+describe("render method", () => {
+ describe("renderFromStorage", () => {
+ it("renders from a raw document", async () => {
+ const registry = new Registry();
+ registry.add("markdown", Markdown);
+ const rendered = await streamToString(
+ await renderFromStorage(
+ registry,
+ [
+ {
+ component_name: "markdown",
+ args: { markdown: "# Hello" },
+ },
+ ],
+ makeSimpleJDDContext(
+ new FileManager("/tmp", "/uploaded_files")
+ )
+ )
+ );
+ assert.strictEqual(rendered, "<h1>Hello</h1>\n");
+ });
+ });
+});

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 21:21 (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
548029
Default Alt Text
(17 KB)

Event Timeline