Write and structure prose content using TypeScript TSX with strong typing.
- Write prose using TSX syntax
- Define your own element schemas and tags
- Link any elements with "uniques" system (e.g. for cross-referencing)
- Convert raw TSX output into a stable, linkable prose tree
- Storage system for attaching async/heavy/reusable data to elements
TSProse is a parser + structurer: you write content in TSX, TSProse turns it into a typed element tree. Rendering, styling, and analysis are up to you.
Think of TSProse as a TeX but written in TypeScript. It is a foundation for writing prose. What tags you define and what you do with the prose (render to site or pdf, analyze, serialize, etc) is up to you.
npm install tsproseConfigure your tsconfig.json to use TSProse as the JSX runtime:
The idea is simple โ you define schemas and tags, use tags to write raw prose in TSX, convert raw prose to prose with stable IDs and collected uniques, then do whatever you want with the prose tree (render, analyze, serialize, etc):
- Schema โ Describes an elementโs shape and rules (block vs inliner, linkable or not, typed data/children).
- Tag (TSX) โ The callable you use in TSX files; it creates/configures raw elements according to the schema.
- RawElement โ The initial, unprocessed elements produced by evaluating your TSX.
- ProseElement (via
rawToProsefunction) โ raw elements are transformed into prose elements viarawToProse()which assigns stable IDs to linkable elements and collects uniques. - (optional) Storage โ Attach async/heavy/reusable data to elements (e.g. computed metadata) when needed.
- Use the prose tree โ Render, analyze, serialize, etc.
This example defines two elements:
paragraph(block, linkable, optional data)bold(inliner, non-linkable)
import {
defineSchema,
defineTag,
defineDocument,
rawToProse,
ensureTagInlinerChildren,
ensureTagChildren,
type BlockSchema,
type InlinerSchema,
type TextSchema,
} from 'tsprose';
//
// 1. Schemas
//
interface ParagraphSchema extends BlockSchema {
name: 'paragraph';
type: 'block';
linkable: true;
Data: { center?: boolean } | undefined;
Storage: undefined;
Children: InlinerSchema[];
}
const paragraphSchema = defineSchema<ParagraphSchema>({
name: 'paragraph',
type: 'block',
linkable: true,
});
interface BoldSchema extends InlinerSchema {
name: 'bold';
type: 'inliner';
linkable: false;
Data: undefined;
Storage: undefined;
Children: TextSchema[];
}
const boldSchema = defineSchema<BoldSchema>({
name: 'bold',
type: 'inliner',
linkable: false,
});
//
// 2. Tags
//
const P = defineTag({ tagName: 'P', schema: paragraphSchema })<{ center?: true }>(
({ tagName, props, children, element }) => {
ensureTagInlinerChildren(tagName, children);
element.children = children;
if (props.center) {
element.data = { center: true };
}
},
);
const B = defineTag({ tagName: 'B', schema: boldSchema })(({ tagName, children, element }) => {
// `text` is built-in; strings become text elements automatically.
ensureTagInlinerChildren(tagName, children);
element.children = children;
});
//
// 3. Document (TSX)
//
const document = defineDocument('my-document-id', {
uniques: { intro: P },
})(({ uniques }) => (
<>
<P $={uniques.intro} center>
Hello <B>world</B>
</P>
</>
));
//
// 4. Convert to prose (stable IDs + collected uniques)
//
const { prose, uniques } = await rawToProse({ rawProse: document.rawProse });
console.log(prose.schema.name); // "mix" (fragment)
console.log(uniques.intro.id); // "intro"- Walk the tree (
walkPre*/walkPost*) to render/analyze - Serialize (
toJSON/fromJSON) if you need persistence - Attach async data via storage (
fillProseStorage) when you need it
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "tsprose", }, }