技术简介
Pintora 通过合理的分层和抽象,为图表作者建立一套从 DSL 解析到图形绘制的简化工具链。
工作流程和数据
Pintora 的工作流程和数据如下所示,
 Input Text
    |
    |  () IDiagramParser
    v
 DiagramIR
    |
    |  () IDiagramArtist
    v
 GraphicsIR
    |
    |  () IRenderer
    v
  Output
其中的 IR 是中间表示(Intermediate Representation)的缩写,代表了不同阶段的数据格式。
DiagramIR代表特定图表类型的逻辑数据,与图表的文字 DSL 联系紧密GraphicsIR代表由 Pintora 提供的渲染层视觉描述数据格式
IDiagramParser 和 DiagramIR
IDiagramParser 的作用是从图表的文字 DSL 转换为逻辑数据,为后续的视觉元素构建准备好基础。
例如 Pintora 内建的 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
}
可以使用任何的语法分析工具和技术来实现这一过程,从逐行分析的正则表达式,到各种 parser generator 生成的程序,只要能在 JS 环境下运行就行。
Pintora 的内建图表使用了 nearley.js 用于生成上下文无关的语法解析器,它简单易用,基于改进了的 Earley 算法,性能尚可(尽管也许是主流解决方案中比较慢的,但那是理论上的最坏情况下,实际情况可能好很多。对于小型文本图表 DSL 来说完全够用),而且运行时很小。图表作者可自行选择高效的 parser generator 方案,如 jison / PEG.js ,或是手写分析器。
IDiagramArtist 和 GraphicsIR
IDiagramArtist 将图表逻辑数据转换为视觉描述数据 GraphicsIR,为之后不同平台的 IRenderer 提供输入。
GraphicsIR 的主要部分有:
rootMark为图表的根元素,一定是一个Group类型的标记,所有的其他元素都是它的子元素- 描述图表整体宽高的 
width和height - 一个可选的 
bgColor作为图表的背景色 
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 将视觉元素抽象为不同类型的标记(Mark)。使用属性(Attribute)的集合 attrs 来描述元素的特性,一些(如 x 和 y)为共有属性,同时每种元素也具有自己特有的属性(如 Path 元素的 path)。
除 attrs 外,在标记上也会有有一些特殊的字段,用于描述其他行为。例如用于描述视觉变换的 matrix,或是Group 特有的 children。
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 坐标 */
  x?: number
  /** y 坐标 */
  y?: number
  /** 圆半径 */
  r?: number
  /** 描边颜色 */
  stroke?: ColorType
  /** 填充颜色 */
  fill?: ColorType
  /** 整体透明度 */
  opacity?: number
  /** 线宽 */
  lineWidth?: number
  ...
}
完整的 GraphicsIR 定义请见 pintora 源码。
Pintora 的渲染层目前使用 antv/g ,可输出 canvas 和 svg 两种格式。因此 GraphicsIR 目前的定义与 antv/g 基本一致,同时你也会发现许多术语与 SVG 定义相似。
为了构建出图表完整的视觉表达,artist 需要做的事情有很多,包括生成各种标记、指定颜色、计算布局相关数据等,因此代码量一般是图表实现中最多的部分。
IDiagram 和 diagramRegistry
IDiagram 为一个图表的完整定义接口,实现了该接口的对象在将自己注册进图表集合 diagramRegistry 后,Pintora 便可识别和处理图表描述的输入文本,并转化为特定的图像输出。
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
}
注册一个图表:
import { IDiagram } from '@pintora/core'
import pintora from '@pintora/standalone'
const diagramDefinition: IDiagram = { ... }
pintora.diagramRegistry.registerDiagram(diagramDefinition)
一些其他细节
文本布局
Pintora 使用 canvas.measureText 来计算文本的布局参数,在 Node.js 端使用 jsdom 和其底层依赖的 node-canvas 来做这件事情。
布局库
对于某些图表类型,计算满足图表逻辑特性,且兼具可读性和美观性的布局绝非易事。参考 Mermaid.js 的实现,Pintora 维护了一份 dagrejs/dagre 的 fork - @pintora/dagre。