Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969674
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 09:45 (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547250
Default Alt Text
(12 KB)
Attached To
Mode
R130 jdd
Attached
Detach File
Event Timeline
Log In to Comment