Oマークアップ言語

本稿ではOマークアップ言語 (O Markup Language; OML) を規定します。 ファイルの拡張子は.omlを推奨します。 文書の記述には、後述するアスキーの記号の文字が含まれる 任意の文字コードを使用して構いません。 実装では少なくともUTF-8に対応することを推奨します。

表す文書はノードの系列よりなります。 ノードはテキストないし要素です。 要素には札と0以上の子ノードの系列があります。

左右で対をなすアスキーの記号の文字 (<[{ (}]>)) を左(右)嘴とします。 そうでないアスキーの記号の24種類の文字 !"#$%&'*+,-./:;=?@\^_`|~ を2個ないし1個連ねたものを眼とします。 2文字の眼は1文字の眼に対して優先します。 アスキーの空白に属す文字、すなわち 水平タブ (HT, 0x09)、 改行(LF, 0x0A)、 垂直タブ (VT, 0x0B)、 改ページ (FF, 0x0C)、 行頭復帰 (CR, 0x0D) 半角空白 (SP, 0x20) の1つ以上の系列を頬とします。 なお入力に含まれるすべての空文字は 構文解析に先立って除かれます。

構文解析は逐次に処理されます。 また以降で「潜在的な」としているものは 最終的に要素にならなければテキストになります。 嘴と眼と省略可能な頬の出現は潜在的な左頭です。 省略可能な頬と眼と嘴の出現で後述の要素の作成を試みます。 それ以外の類型の出現はテキストです。

左頭には照応する札を持つものがあります。 この対応付けの集合を語彙とします。 語彙を変更する特別な要素を語彙変化とします。 その既定の左頭は<!です。 左頭に札がもしあれば備わります。

要素の作成の条件は、それより前の系列に最も近い左頭が存在し、 同じ種類の嘴かつ眼が一致するときです。 眼が一致するのは、眼の個数が同じであり、 眼が1つのとき左頭の眼と同じ、 または眼が2つのとき左(右)眼が左頭の右(左)眼と同じときです。 条件を満たすとき、その左頭より後から左眼より前の系列について、 左頭の札が語彙変化であるなら後述の処理を行い、 左頭の札が空でないなら系列を子に持つ要素を作成し、 左頭の札が空でありかつ 潜在的な語彙変化の直接の子要素であるなら潜在的な要素になります。

語彙変化の処理ではその中身の系列のそれぞれについて、 可能なら次の操作を1度まで行います。 すなわち要素であるなら中身の系列について、 テキストなら要素が対応する札とし、 左頭なら左頭への対応付けを要素の左頭に移します。

以上で言語が規定されました。 以降は非規範的な内容です。

特色と補足

本節では簡単に特色と上述の仕様から導かれる留意点を補足します。 この言語では構文の覚えやすさと実装のしやすさと拡張性に焦点を当てています。 また記述に用いられる自然言語 ならびにテキストエンコーディングによらないことを目指します。 したがって一般に用いられるUTF-8を対象にするなら ごく単純な機能を提供する実装はC言語でさえ難しくないでしょう。 他のマークアップ言語で見られるようなエスケープ文字は存在しません。 例えば語彙変化の既定の頭 <! をそのまま書きたいとします。 これは先立って語彙変化を活用し、別の頭に語彙変化の札を移すことで、 その効力を失わせることができます(後述の実例に示されます)。 すべての文字列は適正な文書として解析が行え、 言い換えれば構文誤りは存在しません。 事前に定義された語彙は、その構文の根幹に関わる語彙変化以外は 1つもありません。 見出しや強調や箇条書きなどといった要素は利用者自らが定義し、 構文解析された結果を利用者自らが加工することを期待するものです。

字句と構文の形式化

本節では字句と構文の一部を大まかに形式化します。 なお、語彙にかかわる照応や語彙変化の具体的な処理は、 形式的な文法のみでは表現できないため、本節には含めません。 そのため、拡張バッカスナウア記法(以下Backus–Naur form; EBNF)にしたがって 解析した結果の一部は、 最終的にテキストとして扱われる可能性があることに留意してください。

Document  ::= Node*
Node      ::= TEXT | Element

LBeak     ::= "(" | "<" | "[" | "{"
RBeak     ::= ")" | ">" | "]" | "}"
EyeChar   ::= "!"  | '"' | "#" | "$" | "%" | "&"
            | "'"  | "*" | "+" | "," | "-" | "."
            | "/"  | ":" | ";" | "=" | "?" | "@"
            | "\\" | "^" | "_" | "`" | "|" | "~"
Eye       ::= EyeChar | EyeChar EyeChar
Cheek     ::= (HT | LF | VT | FF | CR | SP)+

Element   ::= LeftHead Content RightHead
LeftHead  ::= LBeak Eye [Cheek]
RightHead ::= [Cheek] Eye RBeak
Content   ::= Node*

実装の指針

以下に構文解析器の実装に関して例示するアルゴリズムです。 ただし上述の規範的な内容に準拠していれば本内容と異なる方針でも構いません。 参照実装ではスタックの扱いがやや異なりますが流れは同様です。 大枠としてはスタックに基づく逐次的な解析、 すなわち文字列の始めから終わりへの走査です。

入力は前処理を施します。 空文字は除去します。 ユニコードであれば、正規化を行うことを推奨します。 改行はLFに正規化することを推奨しますが、 使用上CRLFのままでも問題ありません。 BOMは除去することを推奨します。 この時点で空文字列のものは空の文書です。

字句解析を行います。 走査中の位置で最長一致を行い、次の種別の字句を生成します。 すなわち、LBeak、RBeak、Eye(長さ2を優先)、Cheek、 そしてその他の文字の連続であるTEXTです。

構文解析を行います。 空のスタックを用意し、これには左頭の候補を保存します。 字句の系列を始めから順に読んで次のように処理します。

字句の列の或る位置でLBeakが現れ、直後にEyeが続く場合、 その位置を1つの潜在的な左頭 (LeftHead) として スタックに押し込みます。 保存する情報はLBeakの種類、Eyeの文字列、左頭の位置です。 ここで、Cheekが続くことがありますが、 これは左頭になった暁には左頭の一部となることに留意します。

字句の列の或る位置でEyeの直後にRBeakが現れた場合、 スタックの上から順に、すなわち最新の左頭から先に調べ、 最初に見つかる「嘴の種類が対であり」かつ 「眼が一致する」左頭を探します。 ここでの処理は上述の仕様の通りです。 例えばもし要素になるなら、頬を除き、 この左眼の前までの字句の列をContentとして 得られた子ノードの系列を要素の子とします。 そしてスタックから該当の左頭を取り去ります。 左頭が見つからなければ、 そのEyeとRBeakはテキストとして扱い、TEXTとして累積します。

構文解析がここまで終わったら、 スタックに残った潜在的な左頭は閉じられなかったものとして 最終的にテキストに変換されます。 なお、上述した処理については語彙変化を別途考慮してください。 これにより要素として見なされるようになったり、 見なされなくなったりするものがあります。

実例

本節では実例を示します。 ここではJSONによる出力を併記します。 これにより、本文書を使った適合性試験を行えます。 JSONによる出力は抽象構文木を表す次のJSONスキーマにしたがいます。 これは参照実装の実行プログラムが生成する形式ですが、 準拠する実装では必ずしもこの構造を生成しなくて構いません。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:local:oml-ast.schema.json",
  "title": "Sample AST",
  "type": "array",
  "items": { "$ref": "#/$defs/node" },
  "$defs": {
    "node": {
      "anyOf": [
        { "type": "string" },
        { "$ref": "#/$defs/element" }
      ]
    },
    "element": {
      "type": "object",
      "properties": {
        "label": { "type": "string" },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        }
      },
      "required": ["label", "children"],
      "additionalProperties": false
    }
  }
}

任意の文字列は適正な原稿です。

Case 1:
a
["a"]

著作権表示のマークは要素ではなく、これは眼がないためです。 素の文字列そのままです。

Case 2:
(C)
["(C)"]

語彙になければ要素になりません。

Case 3:
(+a+)
["(+a+)"]

最小の語彙変化は次の通りです。

Case 4:
<!(*a*)!>(*b*)
[{"label":"a","children":["b"]}]

眼が2つのときは次の通り。

Case 5:
<!(:~a~:)!>(:~b~:)
[{"label":"a","children":["b"]}]

語彙変化で移動があった場合、元の語彙はなくなります。

Case 6:
<!(+a+)(* (+ *)!>(+b+)(*c*)
["(+b+)",{"label":"a","children":["c"]}]

要素にならない場合、頬になる可能性があった部分は保たれます。

Case 7:
(+ (* +)
["(+ (* +)"]

要素になる場合、頬は除かれます。

Case 8:
<!(+a+)!>(+ (* +)
[{"label":"a","children":["(*"]}]

語彙変化もまた別の要素に変更できます。

Case 9:
<! <? <! ?> !><? (+a+) ?><!a!>(+b+)
["<!a!>",{"label":"a","children":["b"]}]

変更履歴

2023年5月21日に第1版が書かれました。 着想の元となったのはTeX、XML、Djotです。 2025年4月27日に本言語の頭字語をOMLにしました。 2026年5月17日に第8版となり空白の扱いが追加されました。

使用許諾

Copyright (C) 2023-2026 gemmaro.

Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved.  This file is offered as-is,
without any warranty.