@myrmidon/cod-layout-view
v2.0.2
Published
Codicological layout formulas viewer
Downloads
42
Readme
CodLayoutView
Codicological layout formulas services and view web component in a framework-independent Typescript library.
This library contains services and models representing manuscript layout formulas, and is a generalization inspired by Cadmus codicology layout formulas.
Formulas are text strings which represent in a compact way the size and layout areas of a manuscript page. Whatever the formula, the same model is used as the parse target.
This library provides:
- generic models for codicological layout;
- a layout view custom web component, which can display an interactive view of the layout represented by the formula;
- layout formula services, which can be swapped in the same layout view component.
Quick Start
Quick start for this repository:
- download or clone the repository.
- open a terminal in its folder.
- restore NPM packages (
npm i). The library is a pure TypeScript library with a custom web component, and has no additional dependencies; packages are used only for infrastructure. - run with
npm run start. When started, if this does not happen automatically open your browser at localhost:3000.
Other commands are used for:
- building:
npm run build. - running:
npm run start. - testing:
npm run test.
Model
All the formulas share the same model, which is based on these elements:
dimension: a single numeric value (
CodLayoutValue): this couples a value with an optional label, and a second optional value representing the original (reconstructed) value. Also,isOriginaltells whether the current value also is the original one (which most often is not the case). So, the dimension value can be represented by:- a current value equal to the original one (
isOriginal=true). - a current value not equal to the original one, which can't be reconstructed (
isOriginal=false). - a current value not equal to the original one, which can be reconstructed (
isOriginal=false,originalValuespecified).
- a current value equal to the original one (
size: a pair of dimensions for the sheet's width and height.
span: a dimension measured from the sheet's edge (top or left) along the horizontal or vertical axis. For instance, on the vertical axis you can get 3 spans: top margin (from sheet's top edge), text, bottom margin. Each is a dimension which gets measured from the previous span or from the sheet's edge. Dimensions along the vertical axis are visually represented by horizontal gridlines, and dimensions along the horizontal axis by vertical gridlines. Spans are dimensions having a direction (horizontal or vertical) and a type (in most cases undefined except when it is
text, which defines an area designed to contain text).
So, the page (which has a given height and width) is partitioned into rectangular spans, which can be:
- vertical spans, stacked from top to bottom.
- horizontal spans, stacked from left to right.
Each span has:
- a
value, which defines the distance taken from the left- or upper- span towards right or bottom; - an optional
label, which can be used to better define the purpose of the span, e.g. margin, space for initials, space for text, etc. - a boolean property (
isHorizontal) to tell whether the span is horizontal or vertical; - a
type, which can be used to specify the span's type in relation with the layout. This is equal totextwhen the span contains text.
Consider this mock example:

Here we have:
- size: height=10, width=8.
- vertical spans: top margin (
mt) = 1, text = 7, bottom margin (mb) = 2. The horizontal gridlines here are green. - horizontal spans: left margin (
ml) = 2, initials column (i) = 1, text = 3, right margin (mr) = 2.
Gridlines, as defined by spans (dimensions), form areas at their intersections. Each span creates a gridline, forming a grid where vertical spans create rows (y coordinates), and horizontal spans create columns (x coordinates).
Each intersection of a row and column creates an area with:
y,x: 1-based coordinates (row 1, column 1, etc.)rowIndexes: labels and types from the vertical span (["label", "$type"])colIndexes: labels and types from the horizontal span (["label", "$type"])
In the above diagram, we conventionally number areas with their Y and X values: so the top left area is 1,1; the area at its right is 1,2; the area below it is 2,1; etc.
As spans can be labelled, areas can get a more human-friendly designation by combining the label from the vertical gridline with the label from the horizontal gridline. For instance:
| col 1 | col 2 | col 3 | col 4 |
| -------------- | ------------- | ----------------- | -------------- |
| 1,1=mt_ml | 1,2=mt_i | 1,3=mt_$text | 1,4=mt_mr |
| 2,1=$text_ml | 2,2=$text_i | 2,3=$text_$text | 2,4=$text_mr |
| 3,1=mb_ml | 3,2=mb_i | 3,3=mb_$text | 3,4=mb_mr |
As you can see, here we are using the (Y and X) labels assigned to each span, or their type (preceded by $) when a label is not present. This convention can be used to define colors for each region.
classDiagram
CodLayoutValue : +number value
CodLayoutValue : +boolean isOriginal
CodLayoutValue : +number originalValue
CodLayoutValue : +string label
CodLayoutValue <|-- CodLayoutSpan
CodLayoutSpan : +string type
CodLayoutSpan : +boolean isHorizontal
CodLayoutFormula : +string type
CodLayoutFormula : +CodLayoutUnit unit
CodLayoutFormula : +CodLayoutValue height
CodLayoutFormula : +CodLayoutValue width
CodLayoutFormula : +CodLayoutSpan[] spans
CodLayoutArea : +number y
CodLayoutArea : +number x
CodLayoutArea : +string[] colIndexes
CodLayoutArea : +string[] rowIndexes
CodLayoutSvgOptions : +boolean showVertical
CodLayoutSvgOptions : +boolean showHorizontal
CodLayoutSvgOptions : +boolean showAreas
CodLayoutSvgOptions : +boolean useOriginal
CodLayoutSvgOptions : +boolean showToolbar
CodLayoutSvgOptions : +string vLineColor
CodLayoutSvgOptions : +string hLineColor
CodLayoutSvgOptions : +string textAreaLineColor
CodLayoutSvgOptions : +number vLineWidth
CodLayoutSvgOptions : +number hLineWidth
CodLayoutSvgOptions : +number areaGap
CodLayoutSvgOptions : +string labelColor
CodLayoutSvgOptions : +number labelFontSize
CodLayoutSvgOptions : +string labelFontFamily
CodLayoutSvgOptions : +Map~string,string~ labelColors
CodLayoutSvgOptions : +boolean showValueLabels
CodLayoutSvgOptions : +string valueLabelColor
CodLayoutSvgOptions : +number valueLabelPadding
CodLayoutSvgOptions : +number padding
CodLayoutSvgOptions : +number scale
CodLayoutSvgOptions : +Map~string,string~ areaColors
CodLayoutSvgOptions : +number areaOpacity
CodLayoutSvgOptions : +string fallbackLineStyle
ErrorWrapper~R,E~ : +R result
ErrorWrapper~R,E~ : +E error
ParsingError : +string message
ParsingError : +string input
ParsingError : +number index
ParsingError : +number length
CodLayoutFormulaService : +ErrorWrapper~CodLayoutFormula,ParsingError~ parseFormula(text)
CodLayoutFormulaService : +string buildFormula(formula)
CodLayoutFormulaService : +string buildSvg(formula, options)
CodLayoutFormulaService : +string[] filterLabels(labels)
CodLayoutFormulaService : +object validateFormula(text)
CodLayoutFormulaService : +void downloadSvg(formula, options, filename)
CodLayoutFormulaService <|-- ITCodLayoutFormulaService
CodLayoutFormulaService <|-- BOCodLayoutFormulaService
Error <|-- ParsingErrorUsage
Using in Angular
▶️ (1) 📦 install package: npm i @myrmidon/cod-layout-view.
The component is generic and its formula service is replaceable. Currently there are two services available:
IT(Itinera, default) andBO(Bianconi-Orsini). To specify the service to use, prefix the formula with$followed by the service identifier and a space, e.g.$BO ...formula here...or$IT ...formula here.... If you don't specify a service, the default will beIT(Itinera), as this format predates BO and there is existing production code using IT formulas without any prefix. Note that the service identifier is case sensitive.
▶️ (2) in your component code, import the web component like this:
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@myrmidon/cod-layout-view';
@Component({
selector: 'my-component',
imports: [
// ...
],
// ADD this line:
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})▶️ (3) in your component template, use it like (in this example, layout is a FormControl<string>):
<cod-layout-view
[attr.formula]="layout.value"
style="width: 100%; height: 900px"
></cod-layout-view>Web Components (Custom Elements) typically use attributes for data binding, while Angular components use properties. This is a common integration point to be aware of. That's why you must use
[attr.NAME]rather than[NAME]. In the same way, you can bind[attr.options]for custom options too.
Using in Vanilla HTML
For using the component in vanilla HTML without any framework:
▶️ (1) 📦 install package: npm i @myrmidon/cod-layout-view.
▶️ (2) import the web component in your HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Layout Formula Example</title>
<script type="module" src="./node_modules/@myrmidon/cod-layout-view/dist/index.js"></script>
</head>
<body>
<!-- Your content here -->
</body>
</html>▶️ (3) use the component in your HTML:
<cod-layout-view
formula="250 × 160 = 30 / 5 [170 / 5] 40 × 15 [5 / 50 / 5* (20) 5 / 40] 5 / 15"
style="width: 100%; height: 800px;">
</cod-layout-view>▶️ (4) optionally, customize with options:
<cod-layout-view
formula="$BO mm 336 x 240 = 18:mt // 282 // 36:mb x 25:ml / 4:i // 174 // 4:i / 33:mr"
options='{"showVertical": true, "showHorizontal": true, "showAreas": true}'
style="width: 100%; height: 800px;">
</cod-layout-view>In vanilla HTML, you can set attributes directly as strings. For the
optionsattribute, pass a JSON string with your configuration.
Formulas
Codicological layout formulas represent the layout scheme of a page with:
- the page size (height and width);
- a set of vertical measurements representing layout areas vertically stacked on the page from top to bottom. Each can be represented as a rectangle whose height is the measurement's value.
- a set of horizontal measurements representing layout areas horizontally stacked on the page from left to right. Each can be represented as a rectangle whose width is the measurement's value.
- usually, we distinguish between areas designed to contain text and areas for other content (or just for margins).
- also, often we add the possibility of representing the current dimension vs. its original value. In most cases pages were trimmed, so that the current dimensions are not equal to the original ones, which in many cases can be reconstructed. This implies that each measurement can have an associated reconstructed value.
The vertical measurements (defining horizontal grid lines from top to bottom) combined with the horizontal measurements (defining vertical grid lines from left to right) define rectangular areas, which represent the layout of a page.
Each service implementing a layout formula provides its own parser for its specific formula. This parser converts a string into a formula object with a shared model. Then, a generic SVG builder can generate its rendition and is used in the custom web component to display a formula. So, SVG rendering options are the same whatever the formula used.
SVG Options
SVG rendering options (CodLayoutSvgOptions) are:
showVertical: true to show vertical gridlines.showHorizontal: true to show horizontal gridlines.showAreas: true to show areas defined by combining vertical and horizontal gridlines.useOriginal: use original values for dimensions when available. When not available, gridlines will use the current value and will be dotted.showToolbar: show toolbar for toggling gridlines and areas.vLineColor: color of vertical lines.hLineColor: color of horizontal lines.textAreaLineColor: color of text area lines.vLineWidth: width of vertical lines.hLineWidth: width of horizontal lines.areaGap: gap between lines.labelColor: label's color.labelFontSize: labels' font size.labelFontFamily: label's font family.labelColors: customized colors for each label.showValueLabels: show value labels on gridlines.valueLabelColor: value label color.padding: padding.scale: scale (unit to pixels).areaColorsdefault: the default area color, used when no other color is specified.- key=value map for other areas. Each area is identified in any of the following ways:
@y_x: Y and X values (see the sample diagram above), separated by underscore.row_col: the label/type from vertical and horizontal gridlines, separated by underscore.row_: the label/type from vertical gridlines only._col: the label/type from horizontal gridlines only.
areaOpacity: the area opacity.fallbackLineStyle: line style used when original values are requested but we are falling back to the current values.
The areaColors object allows you to colorize specific areas using flexible naming. You can target areas using these formats:
@y_x: exact coordinates: "@2_3" (row 2, column 3);row_col: label intersection: "$text_i" (text row × initials column);row_: entire row: "mt_" (all areas in margin-top row);_col: entire column: "_i" (all areas in initials column).
Example:
const options = {
areaColors: {
default: "white",
"$text_$text": "lightgray", // Text×text intersections
"mt_": "lightblue", // All margin-top areas
"_i": "lightyellow", // All initial areas
"@2_3": "red" // Specific area at row 2, col 3
}
};Itinera (IT)
This formula service implements the original Itinera codicological layout formula syntax, which predates Bianconi-Orsini formula. To use the IT formula service, prefix your formula with $IT, or just omit the prefix, because this is the default service. This ensures compatibility with existing data from the Itinera project, where the only formula service was IT and there were no alternatives.
The IT formula syntax follows this pattern: H × W = height_details × width_details, where:
- height details follow the pattern:
mt[/he][ah][/fe]mbormt[hw/]ah[fw/]mb:mt(label=margin-top): margin-top.he(label=head-e): header before the main text area: head-empty (optional) orhw: head-written (optional).ah(label=area-height): area-height (main text area).fe(label=foot-e): footer after the main text area: foot-empty (optional) orfw: foot-written (optional).mb(label=margin-bottom): margin-bottom.
- width details follow the pattern:
ml[columns]mr:ml(label=margin-left): margin-left.colN: optional columns, each having:cleorclw(labelcol-N-left-eorcol-n-left-w) (optional): column left margin.cw(labelcol-N-width): column width (type is alwaystext).creorcrw(labelcol-N-right-eorcol-N-right-w) (optional): column right margin.col-N-gap(gap; label=col-N-gap): gap between two columns. This separates columns. The gap for column 1 is in a span after column 1 and before column 2, and so forth.
mr(labelmargin-right).
These spans are separated by the following structural separators:
- the first
xseparates height and width in size. =separates size from details about height and width./separates margins from vertical or horizontal text areas as well as header, footer, and column margins.- the second
xseparates height details from width details. []wrap spans which contain text (type=textrather than undefined). Square brackets[or]replace/. Empty (non-textual) spans inside[]are marked by a*suffix, which overrides the meaning of[]inverting it.
The diagram below shows the formula's structure using an example (portions marked with - and + are reciprocally exclusive; !=required, ?=optional):
240 × 150 = 30 / 5 [5 / 170 / 5] 5 / 40 × 15 / 5 [5 / 50 / 5* (20) 5* / 40 / 5] 5 / 15
----++++ +++++---- ----++++ - || - ++++----
hhh www hhhhhhhhhhhhhhhhhhhhhhhhhhh wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
1111111111111111 || 22222222222222
h w mt he hw ah fw fe mb ml cle clw cw crX cg clX cw crw cre mr
! ! ! ? / ? ! ? / ? ! ! ? / ? ! ? ! ? ! ? / ? !
height:
[mt ]
[he/hw]
[ah ]
[fe/fw]
[mb ]
width:
col1 col2
[ml] [cle/clw][cw][cre/crw] [cg][cle/clw][cw][cre/crw]... [mr]For example, the formula 250 × 160 = 30 / 5 [170 / 5] 40 × 15 [5 / 50 / 5* (20) 5 / 40] 5 / 15 is rendered as:

Other examples:
Single column with all dimensions:
250 × 160 = 30 / 5 [170 / 5] 40 × 15 [3 / 50 / 5] 15 hhh www hhhhhhhhhhhhhhhhhhh wwwwwwwwwwwwwwwwww mt he ah fw mb ml cl cw cr mr- height: 250, Width: 160.
- height details: mt=30, he=5, ah=170, fw=5, mb=40.
- width details: ml=15, col-left-w=3, col-width=50, col-right-w=5, mr=15.
Two-column layout with gap:
250 × 160 = 30 / 5 [170 / 5] 40 × 15 [5 / 50 / 5* (20) 5 / 40] 5 / 15 hhh www hhhhhhhhhhhhhhhhhhh wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww mt he ah fw mb ml 11111111111 gap 222222222 mr cle cw cre clw cw cre- same height structure as above.
- width details: ml=15, col1(left-w=5, width=50, right-e=5), gap=20, col2(left-w=5, width=40, right-e=5), mr=15.
- the
*indicates empty (non-text) areas.
Simple layout without head/foot:
200 × 160 = 30 [130] 40 × 15 [60 (10) 60] 15 hhh www hhhhhhhhhhh wwwwwwwwwwwwwwwwww mt ah mb ml 11 gap 22 mr- height: mt=30, ah=130, mb=40.
- width: ml=15, col1-width=60, gap=10, col2-width=60, mr=15.
The IT service automatically assigns appropriate labels to spans:
- height spans:
margin-top,height-e,height-w,area-height,foot-w,foot-e,margin-bottom. - width spans:
margin-left,margin-right,col-1,col-2, etc., with suffixeslandrfor left/right margins. - text areas are marked with
type: "text", empty areas have no type.
Bianconi-Orsini
This formula targets size and areas and is derived from D. Bianconi, I Codices Graeci Antiquiores tra scavo e biblioteca, in Greek Manuscript Cataloguing: Past, Present, and Future, edited by P. Degni, P. Eleuteri, M. Maniaci, Turnhout, Brepols, 2018 (Bibliologia, 48), 99-135, especially 110-111.
The formula always targets a recto page used as the sample. Its parts are:
- size:
- unit, e.g.
mm. - height x width:
H [H] x W [W], whereHand/orWcan be wrapped in()(current dimensions not corresponding to the original ones). Each can be followed by another dimension in[]which is the reconstructed dimension. If a dimension is missing, it is replaced by-(here we use a dash rather than an EM dash for better accessibility); from a practical point of view, this-is thus equal to0.
- unit, e.g.
=followed by horizontal spans. Each measurement number here can be wrapped in()and followed by another measurement in[]as above (1.2).x(or×U+00D7) followed by vertical spans, as above (2).
For 2-3 each measurement can be separated by:
/for single ruled areas (=this marks the start of a new area);//for the writing area (=this occurs in pairs, delimiting the writing area).
In addition, a short label (a string without spaces) can be added after each measurement prefixed by :. This addition is required by the generic layout formula model and allows to customize the interpretation of non basic areas (see example 2 below).
For example, the formula $BO mm 336 x 240 = 18:mt // 282 // 36:mb x 25:ml / 4:i // 174 // 4:i / 33:mr is rendered as:

Other examples (see pp.110-111):
(1) mm (57) [175] x (145) [150] = (22) // (35) [115] // - x 10 // 115 // (20)
mm: unit(57) [175]: heightx (145) [150]: width= (22): top margin height// 35 [115]: writing area height// -: missing bottom marginx 10: internal margin width// 115 //: writing area width20: external margin width
(2) mm 336 x 240 = 18 // 282 // 36 x 25 / 4 // 174 // 4 / 33
mm: unit336: heightx 240: width= 18: top margin height// 282 //: writing area height36: bottom margin heightx 25: internal margin width4: column for initials width// 174 //: writing area width4: column for initials width/ 33: external margin width
Here we might use a label to tag the initials column area, e.g.
mm 336 x 240 = 18 // 282 // 36 x 25 / 4:initials // 174 // 4:initials / 33.
(3) mm (245) x (162) = (10) // 206 // (29) x (21) // 114 // (27)
mm: unit245: heightx 162: width= (10): top margin height// 206 //: writing area height(29): bottom margin heightx (21): internal margin width// 114 //: writing area width(27): external margin width
Note that the above examples were fixed as they seem to have typos in the original document (see nr.2 and 3).
Dev Workspace Setup
These steps were used to build this workspace for a pure Typescript library:
create a new folder.
enter it and run
npm init.install these packages:
npm i typescript npm i --save-dev lite-server npm i --save-dev concurrently npm i --save-dev jest npm i --save-dev @types/jest npm i --save-dev ts-jestcreate in
.vscodetasks.json for building (ctrl+shift+B in VSCode) and launch.json for debugging:add main entrypoint, script commands and exports in package.json.
add index.html to the root to host your controls for testing, e.g.:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test Component</title> <script type="module" src="./dist/components/mock.component.js"></script> <script type="module" src="./dist/services/roman-number.js"></script> </head> <body> <h1>Library Development Shell</h1> <article> <h2>Mock component</h2> <div style="border: 1px solid silver"> <mock-component></mock-component> </div> </article> </body> </html>Optionally you can also add some JS client code by adding
index.jsand importing it into this page.add your code (services and components) under
src.export all the required objects from the
src/index.tsAPI entrypoint.
Lite Server
The lite-server does not automatically rebuild your TypeScript files when they change. It only refreshes the browser when HTML or JavaScript files change.
To have your TypeScript files automatically recompile when you make changes, you can use tsc --watch command. This command starts the TypeScript compiler in watch mode; the compiler watches for file changes and recompiles when it sees them.
Then, you can run npm run watch in a separate terminal to start the TypeScript compiler in watch mode.
However, this will not refresh your browser when your TypeScript files are recompiled. To do this, you can use a tool like concurrently to run both lite-server and tsc --watch at the same time, and have lite-server refresh the browser whenever your compiled JavaScript files change.
Once installed this Update your start script to run both commands:
"scripts": {
"start": "concurrently \"tsc --watch\" \"lite-server\""
}Now, when you run npm start, it will start both the TypeScript compiler in watch mode and lite-server. When you make changes to your TypeScript files, they will be automatically recompiled, and lite-server will refresh your browser.
History
2.0.2
- 2025-10-14:
- fix to BO parser for formula type prefix.
- updated packages.
2.0.1
- 2025-07-16: added
hintto formula services.
2.0.0
- 2025-07-08: refactored
parseFormulato avoid throwing and returning either a result or an error.
1.1.1
- 2025-07-07: fixes to IT
buildFormula.
1.1.0
- 2025-07-07:
- refactored SVG rendition so that it is now a shared function independent from the formula being used.
- added download as SVG image to renderer component.
1.0.8
- 2025-07-06: added
validateFormula.
1.0.7
- 2025-07-05:
- fixes to rendition.
- parser throws only
ParseErrorexceptions. - added validation.
1.0.5
- 2025-07-05:
- fixed labels.
- fixed zoom.
1.0.3
- 2025-07-04: added
filterLabelsto formula services.
1.0.1
- 2025-07-03:
- added more methods to formula service interface.
- added
createLayoutFormulaServicefunction.
1.0.0
- 2025-06-30:
- updated dev dependencies.
- added IT (Itinera) formula service implementing the original Angular service syntax.
- updated component to support multiple formula types with
$TYPEprefix. - added comprehensive test suite for IT formula service.
- updated demo with examples of both BO and IT formulas.
0.0.1
Original version with single (BO) formula.
