Contributing to docen
July 4, 2026 · View on GitHub
Thanks for contributing! This guide covers the workflow for contributing and the coding standards that keep docen consistent. For architectural context (data models, API layering, design decisions), see CLAUDE.md.
Development Setup
pnpm install # install dependencies
pnpm build # build all packages
cd packages/<pkg> && pnpm build # build one package
vp check # lint & format
Prerequisites: Node.js 18+, pnpm 9+.
Contribution Workflow
- Fork & clone — fork on GitHub, clone your fork, add
upstream(git remote add upstream https://github.com/DemoMacro/docen.git). - Branch — branch off
main(feat/...,fix:...,docs/..., …). - Code — follow the standards below; match existing style.
- Verify —
vp checkpasses;pnpm buildsucceeds for the changed package. - Commit — use conventional commits:
feat:,fix:,docs:,refactor:,perf:,test:,build:,ci:,chore:,revert:. - Push & PR — push to your fork and open a PR against
upstream/main.
Project Structure
packages/
docen/ docen (all-in-one aggregate entry — re-exports @docen/docx + @docen/editor)
vue/ @docen/vue (Vue 3 adapter — <DocenDocument> component: v-model + v-slot editor)
editor/ @docen/editor (multi-editor host + add-ins: Fluent UI surfaces + @docen/docx → <docen-document>; owns pagination)
docx/ @docen/docx (Tiptap DOCX editor + converters + custom extensions)
- @docen/editor — multi-editor host + add-ins: Fluent UI surfaces (under
src/ui/) + docx engine. - @docen/docx — engine + converters, no UI.
See CLAUDE.md → Package Layout for the file-level tree.
Coding Standards
Naming
- Functions: camelCase with a semantic prefix —
parse*/generate*(external-format I/O),resolve*(DocOpts→JSON),compile*(JSON→DocOpts),create*(factories) - Files & directories: kebab-case
- Interfaces: PascalCase, no
Iprefix,Optionssuffix,readonlyproperties - Constants:
as constobjects (notenum), SCREAMING_SNAKE_CASE keys, lowercase values
export const AlignmentType = {
LEFT: "left",
CENTER: "center",
RIGHT: "right",
JUSTIFY: "justify",
} as const;
Loops
| Scenario | Use |
|---|---|
| Transform into new array | .map() |
| Filter | .filter() |
| Side-effects, async, early exit | for...of |
| Hot paths | for...of / for |
Avoid .forEach() — for...of is strictly superior.
Adding DOCX Features
The runtime model is Tiptap JSON; the persistence model is DocumentOptions (OOXML). Converters bridge the two. See CLAUDE.md → Data Model & API Layering for the data flow.
Converter pattern
DocxManager (converters/docx.ts) walks the tree and assembles DocumentOptions. An extension contributes its DOCX expression by scope:
| Scope | Extensions | Contribution |
|---|---|---|
| Single-node | paragraph, heading, image, table, text-style, strike | export renderDocx(node) / parseDocx(opts) — dispatched per node |
| Cross-node / container | blockquote, lists, task-item, mention, details | export helpers — DocxManager orchestrates multi-node assembly |
| Simple constant | page-break, column-break | payload inlined in DocxManager |
Extension pattern
Custom extensions extend @tiptap/extension-* to carry DOCX properties:
- Attrs with
parseHTMLonly (no attribute-level renderHTML for nodes) - Node-level
renderHTMLcomputes all CSS at once (avoids style-merge conflicts) renderDocx/parseDocxfor DOCX serialization (single-node only)
Mark extensions (text-style, strike) keep attribute-level renderHTML.
export function renderDocx(node: JSONContent): ParagraphOptions {
/* … */
}
export function parseDocx(opts: ParagraphOptions): Record<string, unknown> {
/* … */
}
export const Paragraph = BaseParagraph.extend({
addAttributes() {
return {
...this.parent?.(),
indent: { default: null, parseHTML: (el) => el.style.marginLeft || null },
};
},
renderHTML({ node, HTMLAttributes }) {
const styles = renderParagraphStyles(node.attrs);
const attrs = styles.length ? { ...HTMLAttributes, style: styles.join(";") } : HTMLAttributes;
return ["p", attrs, 0] as const;
},
renderDocx,
parseDocx,
});
Pagination conventions (C-route)
doc > page+, fixed-height page boxes, physical reflow. See CLAUDE.md → Pagination for the architecture.
- Page node is round-trip transparent — never enters DOCX.
DocxManageroperates on flatdoc > block+; the page node exists only at the editor layer. Do NOT add page-node handling toDocxManager. (pageBreak/sectionBreakARE semantic nodes that round-trip.) - Fixed page box —
.docen-page { height: <content area>; overflow: hidden }. Useheight, notmin-height(min-height lets content stretch the page). - Reflow — break at block boundaries first (whole paragraph), then whole table rows; never mid-glyph. Binary-search the break. Debounce + cache measurements (DOM
offsetHeightis ground truth). - Paragraph rules (Word defaults) — widow/orphan control, keepNext (heading + next block), keepLines.
- Table across pages — whole-row move; clone
tableHeaderon continuation pages; clip + warn for over-tall rows (no infinite loop). Mid-row split is out of scope (see CLAUDE.md → Fidelity boundary).
Pull Request Checklist
-
vp checkpasses -
pnpm buildsucceeds for the changed package - Naming & patterns follow the standards above
- Changes are minimal and focused — match existing style