Login
BML Specification
12/11/2020, 11:30:18 AM

Block markup language (BML) is a (pseudo?) markup language used in ZCMS. The syntax is like:

<md boolattr attr="value">
## somthing...
</md>
<code[id]>
only the end tag with the same name and [id] will be
regarded as valid end tag.
</code[id]>

Here are Specification for it.

WIP WARNING⚠

# BML Specification version 0.0.0
# `#` is for comment
# BML is case-sensitive, and all bml declaration MUST be
# in an independent line, with NO trailing white characters.
# 1. start tag
# start tag is matched with /^<\s*([a-zA-Z]+)(?:\[(\w+)\])?((?:\s+(?:[a-zA-Z]+)(?:\s*=\s*[\S]+)?)*)\s*>/
# here are some valid start tags
<TAGNAME>
<TAGNAME[TAGID]>
<TAGNAME ATTRS>
<TAGNAME[TAGID] ATTRS>
# 2. attrs
# attrs is matched with /(?:\s+([a-zA-Z]+)(?:\s*=\s*([\S]+))?)/g
# here are some valid attrs
key       # only key without value is regarded as boolean value true
key="str"
key='str' # strings
key=1     # numbers
# 3. end tags
# end tag is matched exactly with start tag and its id
# examples:
<md>
</md> # Good
<md[test]>
</md> # Won't match
</md[test]> # Good
# 4. plain text
# plain text between blocks is parsed into a `text` block.
# 5. plain comment
# lines between blocks that starts with `//` is regarded as
# comment and ignored.
Current BML parser implementation:
interface IBlock {
  type: string
  content: string
  id?: string
  props?: Record<string, any>
}

export function parse(bml: string) {
  if (typeof bml !== 'string' || !bml) return []

  const lines = bml.split('\n')
  const blocks: IBlock[] = []

  let cur: IBlock | undefined
  let contentLines: string[] = []
  let textLines: string[] = []

  function finalizeTextBlock() {
    if (textLines.length) {
      while (textLines.length && !textLines[textLines.length - 1].trim())
        textLines.pop()
      blocks.push({
        type: 'text',
        content: textLines.join('\n')
      })
      textLines = []
    }
  }

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    if (cur) {
      const m = line.match(/^<\/\s*([a-zA-Z]+)(?:\[(\w+)\])?\s*>/)
      if (m && m[1] === cur.type && m[2] === cur.id) {
        cur.content = contentLines.join('\n')
        blocks.push(cur)
        cur = undefined
        contentLines = []
      } else {
        contentLines.push(line)
      }
    } else {
      const m = line.match(
        /^<\s*([a-zA-Z]+)(?:\[(\w+)\])?((?:\s+(?:[a-zA-Z]+)(?:\s*=\s*[\S]+)?)*)\s*>/
      )
      if (m && m[1]) {
        finalizeTextBlock()

        const [, type, id, attrs] = m
        const props: Record<string, any> = {}
        if (attrs) {
          const result = attrs.matchAll(
            /(?:\s+([a-zA-Z]+)(?:\s*=\s*([\S]+))?)/g
          )
          for (const m of result) {
            const [, key, value] = m
            if (value === undefined) {
              props[key] = true
            } else {
              props[key] = JSON.parse(value)
            }
          }
        }
        cur = {
          type,
          id,
          props,
          content: ''
        }
      } else if (!line.startsWith('//')) {
        if (textLines.length || line.trim()) {
          textLines.push(line)
        }
      }
    }
  }
  finalizeTextBlock()
  return blocks
}