1. Core Ideas: Concepts & the Tree
Before writing any code, this chapter lays out how CallioText thinks about documents: what a concept is, why concepts come in two levels, what kinds of nodes the document tree holds, and what the editor and the printer are each responsible for. There is no code here, but every later chapter builds on this one, so it is worth reading through.
Starting from LaTeX
Anyone who has written a paper in Word remembers the taste of it: the theorems want a uniform style and consecutive numbers, and all of it is held together by hand. The bold is yours, the "Theorem 3.2" is yours; insert a new theorem before the third one and every number after it expires at once, to be fixed one by one; decide someday to restyle the theorems, and there is nothing for it but to comb through the whole document.
LaTeX users take a different route. In LaTeX, a theorem is written:
\begin{theorem}
...
\end{theorem}
The author says only that a theorem runs from here to there; fonts, numbers and spacing are the style system's worry, and changing the style means changing one definition. Behind this sits an idea worth keeping:
The content and structure of a document should be separate from its appearance. Authors declare structure; appearance is handled uniformly by rendering rules.
LaTeX, however, is a product of the days before the internet: its output is pages prepared for the printer, its input is code that swells along with the document. What CallioText does is carry the same idea into a graphical editor: the interaction feels close to Word, the document's internal structure is as explicit as in LaTeX, its appearance is decided by rendering rules, and the output end is free, a web page or, further on, PDF.
Concepts
The theorem above is what CallioText calls a concept. A concept is a type you define for a
category of document content: theorem, proof, footnote, quotation block, divider, all of these can be
concepts. Each concept specifies two things:
- Which parameters this kind of content has. A theorem might have a title parameter (whether it displays as Theorem, Lemma or Proposition) and a numbering-style parameter.
- How it is rendered. The appearance while editing and the appearance in the output are two separate definitions that do not interfere with each other.
Concepts come in two levels
A key design decision in CallioText is that concepts are split into two levels. In OOP terms, a first-class concept is like a class, and a second-class concept is like an object of that class.
| First-class concept (FirstClassConcept) | Second-class concept (SecondClassConcept) | |
|---|---|---|
| Defined by | The engine, in code | Anyone, at runtime (for example loaded from JSON) |
| Defines | The parameter prototype (which parameters, their defaults) and the rendering logic | Inherits a first-class concept; overrides some parameter defaults, or pins some parameters |
A concrete example. Say a first-class concept primary stands for a passage that deserves
prominent display. It defines three parameters, title, prefix and
ordering, along with the rendering rule: title and number first, body after.
A second-class concept named Proposition inherits primary and sets the default title to
"Proposition"; another one named Project also inherits primary but displays the project's
name as its title. The two share one copy of rendering code and differ only in parameters.
There are two motivations for this design:
- Rendering code is written once per first-class concept, and any number of second-class concepts share it. Maintenance stays cheap.
- Second-class concepts can be created at runtime. Authors, even those who do not write code, can derive new document components themselves: if Proposition is not enough, they can simply create a Conjecture.
Naming convention: first-class concepts take lowercase English names, such as
primary, strong, quote; second-class concepts take real words from
the author's vocabulary, such as Proposition, Emphasis, Quotation.
The document tree: seven kinds of nodes
A document is a tree. Its nodes come in seven kinds; five of them are concept nodes (each must specify which concept it uses), and two are plain content:
| Node | type | Role | Allowed children |
|---|---|---|---|
| Text | text | Leaf node, the actual characters | none |
| Paragraph | paragraph | Container of a run of text | text, inline, support |
| Inline | inline | Inline semantics: emphasis, links, inline formulas | text, inline |
| Group | group | Block-level semantics: theorems, proofs, quotation blocks | paragraph, group, structure, support (no bare text or inline) |
| Structure | structure | Semantics with a fixed internal layout, such as a row split into columns | groups only |
| Support | support | Contentless placeholder elements: dividers, images | none (placeholder empty text) |
| Abstract | abstract | An independent sub-document attached to another node: footnotes, annotations. The document root is also one. | same as group |
Beyond the table, a few rules deserve a mention of their own:
- All text lives inside paragraphs. As the table shows, text nodes cannot appear directly inside groups, structures or abstracts; text goes into a paragraph first. This rule keeps the tree structure regular.
- Block nesting is unrestricted. Groups can contain groups: a quotation block can hold a theorem, the theorem can hold a list, to any depth.
- A support node has no content of its own; an image's URL and width live in its parameters. It can stand on its own as a block (a divider) or sit inside a paragraph as an inline element (an inline image).
- Abstracts recurse. A footnote can be made as an abstract; and an abstract is itself a node, so like any other concept node it can carry abstracts of its own, nesting level after level. The root of every document tree is also an abstract node, a fact that will come up repeatedly.
The fields of a concept node
Every concept node carries a fixed set of fields. Using the group node as the example:
interface GroupNode {
type: "group"
idx: string
concept: string
parameters: ParameterList
children: NonLeafNode[]
abstract: AbstractNode[]
relation: "chaining" | "separating"
}
idx: the node's identifier, unique across the document, generated by the library. Cross references (referring to one place from another) use it to locate their target.concept: the name of the concept this node uses. Note that this is the second-class concept's name, such as "Proposition".parameters: the parameter values. Each carries a type annotation, for example{title: {type: "string", val: "Theorem"}}. The annotation lets the editor build an appropriate editing control for each parameter: a text field for strings, a toggle for booleans.abstract: the list of abstract nodes attached to this node, such as an annotation hung on a theorem.relation: this node's relation to its previous sibling, either chaining or separating.
relation deserves an example. Two adjacent list blocks can be two unrelated lists (separating),
or one list that resumes after being interrupted (chaining). Rendering and numbering can both see the
difference: automatic numbering, for instance, can be configured to restart at separating blocks and to
continue across chaining ones. Authors can switch a block between the two states at any time in the editor.
The editor and the printer
Two parts of the library process this tree: the editor and the printer. The most fitting analogy for their relationship is a compiler. The document tree is an intermediate representation; the editor is the front end, encoding the high-level language (the author's graphical input) into that representation; the printer is the back end, translating the representation into the target language (a web page, or further on, PDF output). Front end and back end talk only through the intermediate representation, which is why the same tree can wear two fully independent appearances.
- The editor: the interface authors work in. Each concept registers an editor renderer here, typically with a title bar, buttons and a parameter panel. The core class on this side is
EditorCore. - The printer: the part that renders the tree into the final output, in the sense of typesetting for print. Each concept registers a printer renderer here, producing clean output suited for reading. The core class is
Printer.
The printer has one important trait: before actually rendering, it first walks the whole tree from start to end in document order (this step is called preprocessing). The reason is straightforward: information like theorem numbers cannot be computed from a single node. The fifth theorem is number 5 only because four theorems precede it, and someone has to count them. Preprocessing computes all such document-wide information up front so that each node can simply read its own result at render time. Automatic numbering and cross references are both built on this step; chapter 6 puts it to use.
Chapter summary
- Structure and appearance are separate: authors declare structure, rendering rules decide appearance.
- Concepts have two levels, like classes and objects: first-class concepts are defined by the engine in code; second-class concepts can be created at runtime without code.
- A document is a tree with seven node kinds; concept nodes carry idx, concept, parameters, abstract and relation.
- The editor and the printer are like a compiler's front end and back end: one encodes graphical input into the document tree, the other translates the tree into typeset output, and they meet only at the tree.
- The printer preprocesses (a full walk computing numbers and collecting references) before rendering.