Technical Brief
Pintora builds a simplified tool chain for diagrammers from DSL-parsing to diagram-drawing through sensible layering and abstraction.
Workflow and data
Pintora's workflow and data are shown below.
 Input Text
    |
    |  () IDiagramParser
    v
 DiagramIR
    |
    |  () IDiagramArtist
    v
 GraphicsIR
    |
    |  () IRenderer
    v
  Output
IR stands for Intermediate Representation and represents the different phases of the processing.
- DiagramIRrepresents the logical data for a specific diagram type, relevant to the textual DSL of the diagram
- GraphicsIRis the visual repesentation format provided by Pintora
IDiagramParser and DiagramIR
The role of the IDiagramParser is to convert the textual DSL of the diagram into logical data, preparing the ground for the subsequent construction of the visual elements.
For example, here is the logical data that corresponds to Pintora's built-in Entity Relationship Diagram.
export type ErDiagramIR = {
  entities: Record<string, Entity>
  relationships: Relationship[]
}
export type Attribute = {
  attributeType: string
  attributeName: string
  attributeKey?: string
}
export type Entity = {
  attributes: Attribute[]
}
export type Relationship = {
  entityA: string
  roleA: string
  entityB: string
  relSpec: RelSpec
}
export type RelSpec = {
  cardA: Cardinality
  cardB: Cardinality
  relType: Identification
}
Any parser tool and technique can be used to implement this process, from line-by-line parsing of regular expressions to programs generated by various parser generators, as long as they can be run in a JS environment.
Pintora's built-in diagrams use nearley.js to generate context-independent syntax parsers that are easy to use, based on an improved Earley algorithm, it has decent performance (though perhaps relatively slow - in worst case - of the mainstream solutions, but perfectly adequate for small text diagram DSLs) and a pretty small runtime. Diagram authors can choose between an efficient parser generator solution such as jison / PEG.js, or a handwritten parser.
IDiagramArtist and GraphicsIR
IDiagramArtist converts diagram logic data into visual description data GraphicsIR, which provides input to the IRenderer for different platforms later.
The main parts of GraphicsIR are
- rootMark, which must be a- Grouptype mark, is the root element of the diagram, and all other elements are its children
- widthand- heightthat describe the overall width and height of the diagram
- an optional bgColorfor the diagram's background color
export type Mark = Group | Rect | Circle | Ellipse | Text | Line | PolyLine | Polygon | Marker | Path | GSymbol
export interface GraphicsIR {
  mark: Mark
  width: number
  height: number
  bgColor?: string
}
Pintora abstracts visual elements into different types of marks. A collection of attributes attrs is used to describe the characteristics of the marks, some (e.g. x and y) are common attributes, while each type of mark has its specific attributes (e.g. path for the Path mark).
In addition to attrs, there are also special fields on the tags that describe other behaviors. For example, matrix for describing visual transformations, or children specific to Group.
export interface IMark {
  attrs?: MarkAttrs
  class?: string
  /** for transform */
  matrix?: Matrix | number[]
}
export interface Group extends IMark {
  type: 'group'
  children: Mark[]
}
export interface Circle extends IMark {
  type: 'circle'
  attrs: MarkAttrs & {
    x: number
    y: number
    r: number
  }
}
/**
 * Common mark attrs, borrowed from @antv/g
 */
export type MarkAttrs = {
  x?: number
  y?: number
  /** radius of circle */
  r?: number
  /** stroke color */
  stroke?: ColorType
  /** fill color */
  fill?: ColorType
  opacity?: number
  lineWidth?: number
  ...
}
You can find the full GraphicsIR definition in pintora's source code.
Pintora's rendering layer currently uses antv/g and can output both canvas and svg formats. So GraphicsIR is currently defined in much the same way as antv/g, and you will also find many terms similar to SVG definitions.
To build a complete visual representation of a diagram, the artist needs to do several things, including generating various markers, specifying colors, calculating layout-related data, etc., so the amount of code is usually the largest part of the diagram implementation.
IDiagram and diagramRegistry
IDiagram is a fully defined interface to a diagram. After an object that implements this interface registers itself into the diagram collection diagramRegistry, Pintora can recognize and process the input text of the diagram description and turn it into a specific image output.
export interface IDiagram<D = any, Config = any> {
  /**
   * A pattern used to detect if the input text should be handled by this diagram.
   * @example /^\s*sequenceDiagram/
   */ 
  pattern: RegExp
  parser: IDiagramParser<D, Config>
  artist: IDiagramArtist<D, Config>
  configKey?: string
  clear(): void
}
/**
 * Parse input text to DiagramIR
 */ 
export interface IDiagramParser<D, Config = any> {
  parse(text: string, config?: Config): D
}
/**
 * Convert DiagramIR to GraphicsIR
 */ 
export interface IDiagramArtist<D, Config = any> {
  draw(diagramIR: D, config?: Config): GraphicsIR
}
To register a new type of diagram:
import { IDiagram } from '@pintora/core'
import pintora from '@pintora/standalone'
const diagramDefinition: IDiagram = { ... }
pintora.diagramRegistry.registerDiagram(diagramDefinition)
Some other details
Text layout
Pintora uses canvas.measureText to calculate the layout parameters for text, and uses jsdom and its underlying dependency node-canvas to do this on the Node.js side.
Layout libraries
For some diagram types, it is not easy to compute layouts that satisfy the logical properties of the diagram, but are also readable and aesthetically pleasing. Inspired by the Mermaid.js' implementation, Pintora maintains a fork of dagrejs/dagre - @pintora/dagre.