I spend most of my web-dev time using React anymore and I've become pretty reliant on its component paradigm. Long story short, I decided to try my hand at writing a system that generates an HTML file that I can feed into prince using a similar component-like pattern.
The benefit of this is that it handles things like building Table of Contents during the emission of the HTML file, rather than having to rely on running javascript within prince. My belief is that it would also allow one to reduce repetition and to give an author a bit more of a semantic set of components to work with (such as having a "<Frontmatter>" component) while still being familiar html-esque territory.
It's not quite ready for general use (the "styled" css injection system is fairly reliant on nesting css that prince doesn't yet support - it's not a deal-breaker, but it's a bit limited in what the styled wrapper can do right now).
anyone interested?
Full transparency, I haven't yet bought Prince, but I plan to as soon as I get this to a good state (I only just started today on this) if only to ensure that it actually works.
For reference, this is what the above code spits out:
The benefit of this is that it handles things like building Table of Contents during the emission of the HTML file, rather than having to rely on running javascript within prince. My belief is that it would also allow one to reduce repetition and to give an author a bit more of a semantic set of components to work with (such as having a "<Frontmatter>" component) while still being familiar html-esque territory.
It's not quite ready for general use (the "styled" css injection system is fairly reliant on nesting css that prince doesn't yet support - it's not a deal-breaker, but it's a bit limited in what the styled wrapper can do right now).
anyone interested?
Full transparency, I haven't yet bought Prince, but I plan to as soon as I get this to a good state (I only just started today on this) if only to ensure that it actually works.
import { jsx, Fragment, render, createRegistry, useRegistry, GrimoireNode, styled, useId } from "@trh/grimoire";
const TableOfContents = createRegistry<{ text: string; element: string }[]>([]);
const TableOfTables = createRegistry<{ text: string; element: string }[]>([]);
const Book = () => {
return (
<>
<Frontmatter>
<TableOfContents component={TocBody} />
<TableOfTables component={TotBody} />
</Frontmatter>
<Chapter title="The Beginnings">
<p>Chapter one's content</p>
<Table title={"Some Table"}>table contents go here...</Table>
</Chapter>
</>
);
};
const Frontmatter = ({ children }: { children?: GrimoireNode }) => {
// global css injected by this component
styled.css`
@page frontmatter {
background-color: #ddd;
}
article.frontmatter {
page: frontmatter;
}
`;
return <article className={"frontmatter"}>{children}</article>;
};
const Chapter = ({ title, children }: { title: string; children?: GrimoireNode }) => {
// add this chapter to the ToC
const uniqueId = useId("outline");
useRegistry(TableOfContents).update((p) => [...p, { text: title, element: uniqueId }]);
return (
<>
<ChapterHead id={uniqueId}>{title}</ChapterHead>
{children}
</>
);
};
const ChapterHead = styled.h2`
font-size: 24pt;
break-before: page;
`;
const Table = ({ title, children }: { title: string; children?: GrimoireNode }) => {
// get a unique ID for this element with prefix of "table";
const uniqueId = useId("table");
// add this table to the ToT
useRegistry(TableOfTables).update((p) => [...p, { text: title, element: uniqueId }]);
return <table id={uniqueId}>{children}</table>;
};
const TocBody = ({ value }: { value: { text: string; element: string }[] }) => {
return (
<>
<h2>Table of Contents</h2>
<div>
{value.map(({ text, element }) => {
return <Entry target={element}>{text}</Entry>;
})}
</div>
</>
);
};
const TotBody = ({ value }: { value: { text: string; element: string }[] }) => {
return (
<>
<h2>Table of Tables</h2>
<div>
{value.map(({ text, element }) => {
return <Entry target={element}>{text}</Entry>;
})}
</div>
</>
);
};
const Entry = styled(({ children, className, target }: { children: GrimoireNode; className?: string; target: string }) => {
return (
<a href={`#${target}`} className={className}>
{children}
</a>
);
})`
display: block;
&::after {
content: target-counter(attr(href), page);
}
`;
const compile = async () => {
const [complete, bodyOnly, stylesOnly] = await render(<Book />, { title: "Some Book", meta: { author: "ThatRobHuman" } });
console.log(complete);
};
try {
compile();
} catch (e) {
console.error(e);
}
For reference, this is what the above code spits out:
<!DOCTYPE html><html><head><title>Some Book</title><meta name="author" content="ThatRobHuman"><style>
@page frontmatter {
background-color: #ddd;
}
article.frontmatter {
page: frontmatter;
}
.grimoire-styled-1 {
font-size: 24pt;
break-before: page;
}
.grimoire-styled-2 {
display: block;
&::after {
content: target-counter(attr(href), page);
}
}</style></head><body><article class="frontmatter"><h2>Table of Contents</h2><div><a href="#outline-1" class="grimoire-styled-2">The Beginnings</a></div><h2>Table of Tables</h2><div><a href="#table-1" class="grimoire-styled-2">Some Table</a></div></article><h2 id="outline-1" class="grimoire-styled-1">The Beginnings</h2><p>Chapter one's content</p><table id="table-1">table contents go here...</table></body></html>
