Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969798
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R130 jdd
Attached
Detach File
Event Timeline
Log In to Comment