Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions SKILLS/nc-design-artcraft-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@ For substantial work, route through `nc-arch` and `nc-hl-design`, then the relev
- Are missing capabilities stated in the UI and in `docs/gaps.md`?
- Are license, attribution, build, and test implications recorded?
- Are directory listings kept distinct from implementation evidence?

## Reference: WordCraft and NoteCraft document models

See `references/wordcraft_x_notecraft_dom.md` for the comparison of the two document models and what it means for caret, runs and inline objects.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# WordCraft and NoteCraft: document models compared (2026-11-12)

Sources:
- NoteCraft: `crates/formats/src/markdown.rs` (`Span`, `Block`), `docs/dom_serialization/page-model.md`,
`SKILLS/nc-dom-ser/SKILL.md`.
- WordCraft: `crates/doc/src/block.rs`, `crates/doc/src/para.rs` (`Paragraph`), `crates/doc/src/para/run.rs`
(`Run`), `crates/doc/src/para/inline_object.rs` (`InlineObject`), `crates/doc/src/position.rs` (`Pos`),
`crates/doc/src/lib.rs` (`Document`).

## Side by side

| Concern | NoteCraft | WordCraft |
|---|---|---|
| Text container | `Paragraph(Vec<Span>)`: each block owns its styled spans | `Paragraph { text: String, runs, objects, props, mark }`: one text string per paragraph |
| Formatting unit | `Span { text, bold, italic, strike, underline, script, code, math, font_size, font_family, link }` | `Run { len, props: CharProps }`: a byte length over the paragraph text, with a full character property set |
| Formatting storage | flags on each span, written into the file per span | properties in a shared style sheet; runs refer to them |
| Inline objects | images are separate blocks (`Block::Image`) | objects sit in the text, marked by `OBJ` (U+FFFC); `InlineObject::Image`, fields, controls |
| Breaks | line and page breaks are parts of the text or separate blocks | `LINE_BREAK` (U+000A), `PAGE_BREAK` (U+000C) in the text |
| Block kinds | Heading, Paragraph, Item, Quote, Code, MathBlock, Table, Rule, Image: one enum | `Block::Para` and `Block::Table`; heading and list are paragraph properties |
| Position | a block index and a character offset in that block's text (`ui.caret`, `Caret { block, text, selection }`) | `Pos { story, path, off }`: a story, a path to the paragraph or cell, and a byte offset |
| Nesting | tables hold spans per cell; no nested boxes | tables and text boxes nest (`MAX_TEXT_BOX_DEPTH = 4`) |
| Sharing | blocks are owned values | blocks are `Arc<Block>`; undo copies only what changes |
| File format | HTML and CMGFM, one page per folder, both formats must match (`nc-dom-ser`) | DOCX (OOXML), with parts keyed by number |

## What this means

1. **Our caret is per block; theirs is per paragraph stream with runs and objects inside it.** A caret
in WordCraft steps across run and object boundaries without special cases, because they are offsets
into one string. In NoteCraft, each block is a unit, so a move across a block edge needs its own rule
for every block kind. This is the root of the Up, Down and Left landing bugs.
2. **Our formatting is coarse.** Flags per span can express what we ship today. They can't express
shared character styles, or a run-level change that doesn't create a new span. Changing a style on a
part of a word means splitting spans in our model, and the file grows.
3. **Inline objects are blocks for us.** An image in the middle of a sentence is a block break. In
WordCraft it is one character in the text.
4. **Positions are different kinds of address.** Ours is `(block, char)`, with the char counted in
characters. WordCraft's is `(story, path, byte)`, with the byte counted in UTF-8 bytes and snapped to
clusters by the layout. Any shared code must agree on one of these, and convert at the edge.

## Options for NoteCraft

- **A. Keep our model.** Fix the caret rules per block kind. Costs little now, and it's where the
current bugs come from.
- **B. A single text stream per paragraph, with runs and object markers,** kept in our own model and
written to both formats. This solves the caret problem at its root. It changes the page file, the
parity rules, and every reader and writer. It needs an owner decision and an ADR.
- **C. Adopt WordCraft's document crate.** Not recommended: it models DOCX, and its story, path and
style sheet are not the NoteCraft folder format.

## Decision needed

Owner: choose A now and B later, or B now. Until then, new caret and text code should keep positions as
`(block, char)` and not assume a block boundary is a text boundary, so that option B stays open.

## Related

- `nc-dom-ser`: the page model and the parity rules that option B would change.
- `nc-uiux-bidi`: the caret model section and `references/caret-options.md`.
- `nc-design-artcraft-integration`: the reuse and integration guidance this comparison supports.
12 changes: 12 additions & 0 deletions SKILLS/nc-dom-ser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,15 @@ fn md_round_trip() {
---

**Last updated:** 2026-10-24 | **Blocking requirement:** Yes

## Reference: WordCraft and NoteCraft document models

See `references/wordcraft_x_notecraft_dom.md` for the comparison of the two document models and what it means for caret, runs and inline objects.

## Reference: object types

See `references/object-types.md` for the object identifiers, Rust names, WordCraft and OneNote equivalents, and the room for future object kinds.

See `references/naming-convention.md` for the rules that the identifiers, Rust names, HTML classes and data attributes follow.

RFC 0001, the proposed NoteCraft document model with its integration appendix: `references/rfc-0001-notecraft-dom.md`.
76 changes: 76 additions & 0 deletions SKILLS/nc-dom-ser/references/naming-convention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Naming convention for page objects (proposal, 2026-11-12)

Status: proposal for owner review. It applies to new object types and to the table in
`object-types.md`. It does not rename anything already in a file.

## Terms

"DOM" in this repo means the semantic page model, not the browser DOM (see
`docs/dom_serialization/README.md`). WordCraft's `crates/doc` uses overlapping names (`Block`, `Frame`,
`ListItem`) with different meanings, so every name in this convention is NoteCraft's own. Research
context: `dom-naming-context.md` (untracked).

## Why

Neither WordCraft nor craft-libs has a written naming scheme for its document model. WordCraft's names
are the Rust type names in `crates/doc`, which are good, but they don't say how a name is stored in a
file. NoteCraft stores pages as HTML and CMGFM, so every name must work in both formats and never change
once written. This convention fixes that.

## The three names of one object

Each object type has three names. They are kept in one place (`object-types.md`).

| Name | Rule | Example (`inline.image`) |
|---|---|---|
| Identifier | `family.kind`, lower case, dot separated, stored in the file | `inline.image` |
| Rust name | PascalCase, WordCraft's word where it has one | `Image` |
| Format form | the HTML tag and class, and the CMGFM syntax, from the table | `img`, `![alt](src)` |

The identifier is the contract. The Rust name and the format form may change with the code; the
identifier may not.

## Rules

1. **Lower case, dot separated, no spaces.** `inline.image`, not `Inline-Image` or `image`.
2. **Families are fixed:** `block`, `inline`, `container`, `onenote`, `ext`. A new family needs an ADR.
3. **Kind words are singular nouns**, taken from WordCraft where it has the object (`image`, `field`,
`equation`, `control`, `bookmark`, `comment`), otherwise from OneNote (`ink`, `tag`, `pagelink`), and
otherwise a plain English noun.
4. **Start and end markers** share the kind and add `.start` or `.end`: `inline.bookmark.start`.
5. **No format words in identifiers.** `html`, `md`, `cmgfm`, `css` do not appear. The format forms
live in their own column.
6. **Class names follow the identifier.** The HTML class for an object is the identifier with dots
turned into hyphens and the prefix `nc-`: `inline.image` becomes `nc-inline-image`. The `nc-`
prefix keeps our classes apart from the reader's own CSS.
7. **Data attributes use the same words.** `data-nc-kind="inline.image"`, so a reader can check the kind
without parsing the class.
8. **Opaque and extension names are reserved.** `ext.opaque` is the fallback. `ext.<vendor>.<kind>` is
reserved for types from other apps; the vendor is a short lower-case name, for example `artcraft`.
9. **Never reuse an identifier.** A removed type keeps its identifier in the table, marked retired, so an
old file still reads.
10. **Renames are new identifiers.** The old one is kept as an alias in the reader until a migration
exists. The alias is written down in `object-types.md`.

## Worked example

Adding an inline callout (a highlighted note inside a paragraph):

- Identifier: `inline.callout`
- Rust name: `Callout` (no WordCraft equivalent; OneNote has no such object, so the plain noun is used)
- HTML: `<span class="nc-inline-callout" data-nc-kind="inline.callout">…</span>`
- CMGFM: no native syntax, so the object is written as a `ext.opaque`-style HTML fragment in CMGFM and
listed in the parity exceptions with its reason. Parity exceptions need an owner decision.
- Table row, round-trip test, parity check, opaque fallback test: as in `object-types.md`, "Checks
before a new object type lands".

## Checks

1. The identifier follows rules 1 to 5 and is in `object-types.md`.
2. The class and data attribute follow rules 6 and 7, and a test reads them back.
3. A retired or renamed identifier has its alias row.

## Decision needed

Owner: accept this convention as NoteCraft's naming rule for page objects, and decide who owns the
`object-types.md` table (suggested: `nc-dom-ser`).
90 changes: 90 additions & 0 deletions SKILLS/nc-dom-ser/references/object-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Object types: identifiers, names and room to grow (2026-11-12)

Status: proposal for owner review. Nothing here changes the file format until an ADR accepts it.

## Naming rules

1. **Identifier.** Every object type has a stable identifier in the form `family.kind`, lower case, dot
separated: `inline.image`, `block.table`, `onenote.ink`. The identifier is what the file stores. It
never changes once written. A renamed type gets a new identifier and the old one stays readable.
2. **Rust name.** PascalCase, the same word WordCraft uses where WordCraft has the object (`Image`,
`Field`, `Equation`, `Opaque`). Where WordCraft has no object, use the OneNote word, or a plain English
one.
3. **Family.** `block` for a whole block, `inline` for an object inside a paragraph's text, `container`
for a box, `onenote` for a type that exists because of OneNote import or OneNote's model, `ext` for a
type from outside NoteCraft (see Future room).
4. **Paired markers.** A start and an end marker share a family and end in `.start` and `.end`, as
WordCraft's `FieldStart`/`FieldEnd` do.
5. **No format-specific names.** The identifier does not say HTML or CMGFM. Each format has a mapping
column below.

## The table

| Identifier | Rust name | WordCraft | OneNote | NoteCraft today | HTML | CMGFM | Layer |
|---|---|---|---|---|---|---|---|
| `block.paragraph` | `Paragraph` | `Block::Para` | paragraph | `Paragraph` | `p` | text | model, formats |
| `block.heading` | `Heading` | paragraph style | heading 1-6 | `Heading` | `h1`-`h6` | `#` | model, formats |
| `block.item` | `Item` | list paragraph | bullet, number, to-do | `Item` | `ul`, `ol`, `li` | `-`, `1.`, `- [ ]` | model, formats |
| `block.quote` | `Quote` | paragraph style | quote | `Quote` | `blockquote` | `>` | model, formats |
| `block.code` | `Code` | paragraph style | code (OneNote has no code block) | `Code` | `pre` | fence | model, formats |
| `block.math` | `MathBlock` | `inline.equation` in a block | equation | `MathBlock` | `p.math` | `$$` | model, formats |
| `block.rule` | `Rule` | paragraph border | line | `Rule` | `hr` | `---` | model, formats |
| `block.table` | `Table` | `Block::Table` | table | `Table` | `table` | GFM table | model, formats |
| `block.container` | `Container` | text box | box and text box | `Container` (`div.nc-box`) | `div.nc-box` | `div.nc-box` | model, formats |
| `inline.image` | `Image` | `InlineObject::Image` | picture | `Image` block (not inline yet) | `img` | `![]()` | model, formats |
| `inline.graphic` | `Graphic` | `InlineObject::Graphic` | picture in text | not yet | `img` | not yet | model |
| `inline.shape` | `Shape` | `InlineObject::Shape` | drawing shape | not yet | `svg` | not yet | model |
| `inline.group` | `Group` | `InlineObject::Group` | group of drawings | not yet | `g` | not yet | model |
| `inline.field` | `Field` | `InlineObject::Field` | date, page number | date stamp (UI only) | `span.field` | not yet | model |
| `inline.field.start` | `FieldStart` | `FieldStart` | n/a | n/a | n/a | n/a | model |
| `inline.field.end` | `FieldEnd` | `FieldEnd` | n/a | n/a | n/a | n/a | model |
| `inline.control` | `Control` | `ControlStart`/`ControlEnd` | n/a (OneNote has none) | not yet | `span.control` | not yet | model |
| `inline.bookmark.start` | `BookmarkStart` | `BookmarkStart` | n/a | n/a | `a#name` | `<a id>` | model |
| `inline.bookmark.end` | `BookmarkEnd` | `BookmarkEnd` | n/a | n/a | n/a | n/a | model |
| `inline.comment.start` | `CommentStart` | `CommentStart` | n/a | n/a | not yet | not yet | model |
| `inline.comment.end` | `CommentEnd` | `CommentEnd` | n/a | n/a | not yet | not yet | model |
| `inline.equation` | `Equation` | `InlineObject::Equation` | equation | inline math `Span` | `span.math` | `$...$` | model, formats |
| `inline.ruby` | `Ruby` | `InlineObject::Ruby` | n/a | not yet | `ruby` | not yet | model |
| `inline.tab` | `PositionalTab` | `InlineObject::PositionalTab` | tab | not yet | `span.tab` | tab char | model |
| `inline.break` | `Break` | `LINE_BREAK`, `PAGE_BREAK` | line break, page break | line break in text | `br` | line break | model, formats |
| `onenote.ink` | `Ink` | n/a | ink drawing | `ink.svg` per page | `ink.svg` link | `ink.svg` link | model, formats |
| `onenote.tag` | `Tag` | n/a | tag (to-do, important, question, and so on) | `Tag` (model) | `span.tag` | `tag` marker | model, formats |
| `onenote.attachment` | `EmbeddedFile` | n/a | embedded file | attachment in `assets/` | `a` | `[]()` link | model, formats |
| `onenote.pagelink` | `PageLink` | n/a | link to a page | not yet | `a` | `[]()` | model, formats |
| `onenote.stamp` | `PageStamp` | n/a | date and time under the title | drawn stamp (UI only) | `p.stamp` | not yet | model, formats |
| `onenote.audio` | `EmbeddedAudio` | n/a | embedded audio | not yet | `audio` | not yet | model, formats |
| `onenote.video` | `EmbeddedVideo` | n/a | embedded video | not yet | `video` | not yet | model, formats |
| `onenote.printout` | `Printout` | n/a | printout | printout page (`pdf.printout-NNN.png`) | `img` | `![]()` | model, formats |
| `ext.opaque` | `Opaque` | `InlineObject::Opaque` | any object we cannot show | not yet | `span.opaque` with its original XML | same | model |

Notes on the table:
- **Not yet** means no type in the model. Adding one is a change to this table first, then the model,
then both formats.
- **n/a** means the source has no such object, or the marker is not needed in that format.
- The OneNote rows are the types NoteCraft already imports or needs for OneNote parity. OneNote's
own names are used where they exist.

## Future room

- **Open kinds.** Readers keep an unknown object as `ext.opaque`, with its identifier and its original
content. An unknown kind is never dropped on save. This is WordCraft's `Opaque` rule.
- **Versioned object payloads.** Each object payload carries a version. A reader that meets a newer
version keeps the object as opaque.
- **Namespaces.** `ext.<vendor>.<kind>` is reserved for types that come from another app (for example
`ext.artcraft.chart`). They round-trip the same way as opaque objects until NoteCraft implements them.
- **Inline or block.** An object is either inline (a character in the text) or a block. Adding a type
means choosing one. The table says which.

## Checks before a new object type lands

1. The identifier is in this table, with its mapping for HTML and CMGFM.
2. A round-trip test: HTML to model to HTML, and CMGFM to model to CMGFM, with the object unchanged.
3. Cross-format parity (`cargo test -p notecraft-formats --test format_parity`).
4. An opaque fallback test: a file with the object read by a build that doesn't know it.
5. The page model doc (`docs/dom_serialization/page-model.md`) names it.

## Decision needed

Owner: accept the identifier scheme and the table as the basis for the next format change, and decide
which rows come first. Suggested first set: `inline.image`, `inline.field`, `inline.equation`,
`inline.break`, `onenote.ink`, `onenote.tag`, `onenote.attachment`, `onenote.pagelink`, `ext.opaque`.
Loading
Loading