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

  1. Fork & clone — fork on GitHub, clone your fork, add upstream (git remote add upstream https://github.com/DemoMacro/docen.git).
  2. Branch — branch off main (feat/..., fix:..., docs/..., …).
  3. Code — follow the standards below; match existing style.
  4. Verifyvp check passes; pnpm build succeeds for the changed package.
  5. Commit — use conventional commits: feat:, fix:, docs:, refactor:, perf:, test:, build:, ci:, chore:, revert:.
  6. 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 I prefix, Options suffix, readonly properties
  • Constants: as const objects (not enum), SCREAMING_SNAKE_CASE keys, lowercase values
export const AlignmentType = {
  LEFT: "left",
  CENTER: "center",
  RIGHT: "right",
  JUSTIFY: "justify",
} as const;

Loops

ScenarioUse
Transform into new array.map()
Filter.filter()
Side-effects, async, early exitfor...of
Hot pathsfor...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:

ScopeExtensionsContribution
Single-nodeparagraph, heading, image, table, text-style, strikeexport renderDocx(node) / parseDocx(opts) — dispatched per node
Cross-node / containerblockquote, lists, task-item, mention, detailsexport helpers — DocxManager orchestrates multi-node assembly
Simple constantpage-break, column-breakpayload inlined in DocxManager

Extension pattern

Custom extensions extend @tiptap/extension-* to carry DOCX properties:

  1. Attrs with parseHTML only (no attribute-level renderHTML for nodes)
  2. Node-level renderHTML computes all CSS at once (avoids style-merge conflicts)
  3. renderDocx / parseDocx for 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. DocxManager operates on flat doc > block+; the page node exists only at the editor layer. Do NOT add page-node handling to DocxManager. (pageBreak/sectionBreak ARE semantic nodes that round-trip.)
  • Fixed page box.docen-page { height: <content area>; overflow: hidden }. Use height, not min-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 offsetHeight is ground truth).
  • Paragraph rules (Word defaults) — widow/orphan control, keepNext (heading + next block), keepLines.
  • Table across pages — whole-row move; clone tableHeader on 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 check passes
  • pnpm build succeeds for the changed package
  • Naming & patterns follow the standards above
  • Changes are minimal and focused — match existing style