Page MenuHomeSealhub

No OneTemporary

diff --git a/src/component.ts b/src/component.ts
index 241b5ba..6e387bf 100644
--- a/src/component.ts
+++ b/src/component.ts
@@ -1,185 +1,189 @@
import { FlatTemplatable } from "tempstream";
import {
ComponentArgument,
ExtractStructuredComponentArgumentsParsed,
ExtractStructuredComponentArgumentsReceived,
ExtractStructuredComponentArgumentsStorage,
JDDContext,
} from "./index.js";
import slug from "slug";
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 type JDDHeading = { text: string; level: number; id?: string };
export type ComponentToHTMLArgs<
T extends Record<string, ComponentArgument<unknown>>
> = {
args: ExtractStructuredComponentArgumentsParsed<T>;
classes: string[];
jdd_context: JDDContext;
index: number;
};
export abstract class Component<
ArgumentsT extends Record<string, ComponentArgument<unknown>> = Record<
string,
ComponentArgument<unknown>
>
> {
abstract getArguments(): ArgumentsT;
abstract toHTML(
params: ComponentToHTMLArgs<ArgumentsT>
): FlatTemplatable | Promise<FlatTemplatable>;
getTitle(
context: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<ArgumentsT>
): string | null {
return null;
}
getHeadings(
context: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<ArgumentsT>
): JDDHeading[] {
const title = this.getTitle(context, args);
if (title) {
return [{ text: title, level: 1, id: slug(title) }];
} else {
return [];
}
}
+ getCSSClumps(): string[] {
+ return [];
+ }
+
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]) => {
if (!args[key]) {
return [key, null];
}
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] {
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 {
// 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/jdd.test.ts b/src/jdd.test.ts
index 012267a..41d6a3d 100644
--- a/src/jdd.test.ts
+++ b/src/jdd.test.ts
@@ -1,39 +1,53 @@
import assert from "assert";
import { Markdown } from "./components/markdown.js";
import { JDD } from "./jdd.js";
import { Registry } from "./registry.js";
import { simplestContext } from "./test-utils/simplest-context.js";
describe("jdd", () => {
it("properly extracts headings from multiple components", () => {
const registry = new Registry();
registry.add("markdown", Markdown);
const jdd = JDD.fromParsed(registry, simplestContext(), [
{
component_name: "markdown",
args: {
markdown: `
# heading one
## heading two
### heading three`,
},
},
{
component_name: "markdown",
args: {
markdown: `
# heading four
`,
},
},
]);
assert.deepEqual(jdd.getHeadings(), [
{ text: "heading one", level: 1, id: "heading-one" },
{ text: "heading two", level: 2, id: "heading-two" },
{ text: "heading three", level: 3, id: "heading-three" },
{ text: "heading four", level: 1, id: "heading-four" },
]);
});
+
+ it("properly extracts css clumps", () => {
+ const registry = new Registry();
+ registry.add("markdown", Markdown);
+ const jdd = JDD.fromParsed(registry, simplestContext(), [
+ {
+ component_name: "markdown",
+ args: {
+ markdown: `# heading one`,
+ },
+ },
+ ]);
+ assert.deepEqual(jdd.getAllCSSClumps(), ["jdd-component__markdown"]);
+ });
});
diff --git a/src/jdd.ts b/src/jdd.ts
index e27c000..ecbf9ae 100644
--- a/src/jdd.ts
+++ b/src/jdd.ts
@@ -1,216 +1,235 @@
import { tempstream } from "tempstream";
import { JDDContext } from "./jdd-context.js";
import { Registry } from "./registry.js";
import { EarlyAsset, JDDHeading } from "./component.js";
import { hasField } from "@sealcode/ts-predicates";
import {
JDDocumentContainer,
RawJDDocument,
documentContainerFromParsed,
documentContainerFromStorage,
} from "./document.js";
import { documentToParsed } from "./document.js";
export class JDD {
constructor(
public registry: Registry,
public jdd_context: JDDContext,
public parsed: JDDocumentContainer<"parsed">
) {}
static async fromStorage(
registry: Registry,
jdd_context: JDDContext,
document: RawJDDocument
) {
const parsed = await documentToParsed(
registry,
jdd_context,
documentContainerFromStorage(document)
);
return new JDD(registry, jdd_context, parsed);
}
static fromParsed(
registry: Registry,
jdd_context: JDDContext,
parsed: RawJDDocument
) {
return new JDD(
registry,
jdd_context,
documentContainerFromParsed(parsed)
);
}
render(
make_component_classes = (index: number) => [
"jdd-component",
`component-number-${index}`,
]
) {
return tempstream`${this.parsed.value.map(
({ component_name, args }, index) => {
const component = this.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(this.jdd_context);
}
}
return component.toHTML({
args,
classes: make_component_classes(index),
jdd_context: this.jdd_context,
index,
});
}
)}`;
}
renderEarlyScript(asset: EarlyAsset): string {
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>`;
}
}
renderEarlyStyle(asset: EarlyAsset): string {
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>`;
}
}
async renderEarlyAssets() {
const early_assets = (
await Promise.all(
this.parsed.value.map(async ({ component_name, args }) => {
const component = this.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(this.jdd_context);
}
}
return await component.getEarlyAssets(
args,
this.jdd_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") {
return this.renderEarlyScript(asset);
} else if (asset.type == "style") {
return this.renderEarlyStyle(asset);
}
})
.join(" ");
}
countWords(): number {
return this.parsed.value.reduce((acc, { component_name, args }) => {
const component = this.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);
}
getHeadings(): JDDHeading[] {
return this.parsed.value
.map(({ component_name, args }) => {
const component = this.registry.get(component_name);
if (!component) {
return [];
}
return component.getHeadings(this.jdd_context, args);
})
.flat();
}
+ getAllCSSClumps(): string[] {
+ return Array.from(
+ new Set(
+ this.parsed.value
+ .map(({ component_name }) => {
+ const component = this.registry.get(component_name);
+ if (!component) {
+ return [];
+ }
+ return [
+ `jdd-component__${component_name}`,
+ ...component.getCSSClumps(),
+ ];
+ })
+ .flat()
+ )
+ );
+ }
+
static async renderFromStorage(
registry: Registry,
document: RawJDDocument,
jdd_context: JDDContext
) {
const jdd = await JDD.fromStorage(registry, jdd_context, document);
return jdd.render();
}
static async renderEarlyAssetsFromStorage(
registry: Registry,
document: RawJDDocument,
jdd_context: JDDContext
) {
const jdd = await JDD.fromStorage(registry, jdd_context, document);
return jdd.renderEarlyAssets();
}
static async render(
registry: Registry,
parsed: JDDocumentContainer<"parsed">,
jdd_context: JDDContext
) {
const jdd = new JDD(registry, jdd_context, parsed);
return jdd.render();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 09:45 (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547250
Default Alt Text
(12 KB)

Event Timeline