c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

XML を表現する時 XML の階層が表現する code にも表れて欲しい (Python)

偶々 ClojureAtom を、PythonSVG を生成する機會が有った。どちらも XML だ。Python には標準でxml.etree.ElementTreeと云ふ class が含まれてゐて、これで XML を讀み書き出來る。

Clojure だと XML を生成するには、

(require '[clojure.xml :as xml])

(->> {:tag :svg
      :attrs {:xmlns "http://www.w3.org/2000/svg"
              :height "148mm"
              :width "210mm"}
      :content [{:tag :g
                 :content [{:tag :line
                            :attrs {:stroke "black"
                                    :x1 "5mm"
                                    :x2 "200mm"
                                    :y1 "4mm"
                                    :y2 "144mm"}}
                           {:tag :text
                            :attrs {:fill "black"
                                    :font-family "sans-serif"
                                    :font-size "12pt"
                                    :x "25mm"
                                    :y (str (+ 25 (* 12 0.353)) "mm")}
                            :content ["日本語"]}]}]}
     xml/emit
     with-out-str)
<?xml version='1.0' encoding='UTF-8'?>
<svg xmlns="http://www.w3.org/2000/svg" height='148mm' width='210mm'>
<g>
<line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm'/>
<text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'>
日本語
</text>
</g>
</svg>

と map を作ればよい。map は Clojure の得意とする data であるし、map の階層は XML のものと等しい。改行と escape の問題は有る。escape は自前で出來るし、改行に就いてはWhy clojure.xml/emit prints new lines around string contents inside tags? - Stack Overflowの如く patch を当てるかclojure.data.xmlを使ふ事に成る。

これが Python では、

import xml.etree.ElementTree as ET

svg = ET.Element(
    "svg",
    {
        "xmlns": "http://www.w3.org/2000/svg",
        "height": "148mm",
        "width": "210mm",
    },
)
g = ET.SubElement(svg, "g")
ET.SubElement(
    g,
    "line",
    {
        "stroke": "black",
        "x1": "5mm",
        "x2": "200mm",
        "y1": "4mm",
        "y2": "144mm",
    },
)
text = ET.SubElement(
    g,
    "text",
    {
        "fill": "black",
        "font-family": "sans-serif",
        "font-size": "12pt",
        "x": "25mm",
        "y": f"{25 + 12 * 0.353}mm",
    },
)
text.text = "日本語"
ET.tostring(svg).decode("utf-8")
<svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm" /><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">&#26085;&#26412;&#35486;</text></g></svg>

何だこれ。見よこれが OOP だよ。いや Groovy のgroovy.xml.MarkupBuilderも有るが…。

JavaScript も似た樣なものだ。

const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("height", "148mm");
svg.setAttribute("width", "210mm");
const g = document.createElement("g");
svg.appendChild(g);
const line = document.createElement("line");
line.setAttribute("stroke", "black");
line.setAttribute("x1", "5mm");
line.setAttribute("x2", "200mm");
line.setAttribute("y1", "4mm");
line.setAttribute("y2", "144mm");
g.appendChild(line);
const text = document.createElement("text");
text.setAttribute("fill", "black");
text.setAttribute("font-family", "sans-serif");
text.setAttribute("font-size", "12pt");
text.setAttribute("x", "25mm");
text.setAttribute("y", `${25 + 12 * 0.353}mm`);
text.textContent = "日本語";
g.appendChild(text);
svg.outerHTML;
<svg height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm"></line><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>

酷い。これが素で書き下せて嫌に成る。Ruby の REXML も殘念だがこれの類いである。

require 'rexml/document'

REXML::Document.new.tap do |doc|
  doc << REXML::XMLDecl.new('1.0', 'UTF-8')
  doc << REXML::Element.new('svg').tap do |svg|
    svg.add_attributes(
      'xmlns' => 'http://www.w3.org/2000/svg',
      'height' => '148mm',
      'width' => '210mm'
    )
    svg << REXML::Element.new('g').tap do |g|
      g << REXML::Element.new('line').tap do |line|
        line.add_attributes(
          'stroke' => 'black',
          'x1' => '5mm',
          'x2' => '200mm',
          'y1' => '4mm',
          'y2' => '144mm'
        )
      end
      g << REXML::Element.new('text').tap do |text|
        text.add_attributes(
          'fill' => 'black',
          'font-family' => 'sans-serif',
          'font-size' => '12pt',
          'x' => '25mm',
          'y' => "#{25 + 12 * 0.353}mm",
        )
        text.text = '日本語'
      end
    end
  end
end.to_s
<?xml version='1.0' encoding='UTF-8'?><svg height='148mm' width='210mm' xmlns='http://www.w3.org/2000/svg'><g><line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm'/><text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'>日本語</text></g></svg>

Object#tapでごまかした。ごまかせるのは好い事だ。

Goovy だとこう書け、迚も好い。

import groovy.xml.MarkupBuilder

def writer = new StringWriter()
new MarkupBuilder(writer).svg(xmlns:'http://www.w3.org/2000/svg', width:'148mm', height: '210mm') {
  g {
    line(stroke:'black', x1:'5mm', x2:'200mm', y1:'4mm', y2:'144mm')
    text(fill:'black', 'font-family':'sans-serif', 'font-size':'12pt', x:'25mm', y: "${25 + 12 * 0.353}mm", '日本語')
  }
}
writer.toString()
<svg xmlns='http://www.w3.org/2000/svg' width='148mm' height='210mm'> <g> <line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm' /> <text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'>日本語</text> </g> </svg>

Clojure と同じく函數型である Erlang の xmerl を Elixir から使ふと…、

{:ok, pid} = StringIO.open("")

IO.puts(
  pid,
  :xmerl.export_simple(
    [
      {:xmlElement, :svg, :svg, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [], 1,
       [
         {:xmlAttribute, :xmlns, [], [], [], [svg: 1], 1, [], 'http://www.w3.org/2000/svg',
          false},
         {:xmlAttribute, :height, [], [], [], [svg: 1], 2, [], '148mm', false},
         {:xmlAttribute, :width, [], [], [], [svg: 1], 3, [], '210mm', false}
       ],
       [
         {:xmlElement, :g, :g, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [svg: 1],
          1, [],
          [
            {:xmlElement, :line, :line, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []},
             [g: 1, svg: 1], 1,
             [
               {:xmlAttribute, :stroke, [], [], [], [line: 1, g: 1, svg: 1], 1, [], 'black',
                false},
               {:xmlAttribute, :x1, [], [], [], [line: 1, g: 1, svg: 1], 2, [], '5mm', false},
               {:xmlAttribute, :x2, [], [], [], [line: 1, g: 1, svg: 1], 3, [], '200mm', false},
               {:xmlAttribute, :y1, [], [], [], [line: 1, g: 1, svg: 1], 4, [], '4mm', false},
               {:xmlAttribute, :y2, [], [], [], [line: 1, g: 1, svg: 1], 5, [], '144mm', false}
             ], [], [], '', :undeclared},
            {:xmlElement, :text, :text, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []},
             [g: 1, svg: 1], 2,
             [
               {:xmlAttribute, :fill, [], [], [], [text: 2, g: 1, svg: 1], 1, [], 'black', false},
               {:xmlAttribute, :"font-family", [], [], [], [text: 2, g: 1, svg: 1], 2, [],
                'sans-serif', false},
               {:xmlAttribute, :"font-size", [], [], [], [text: 2, g: 1, svg: 1], 3, [], '12pt',
                false},
               {:xmlAttribute, :x, [], [], [], [text: 2, g: 1, svg: 1], 4, [], '25mm', false},
               {:xmlAttribute, :y, [], [], [], [text: 2, g: 1, svg: 1], 5, [], String.to_charlist("#{25 + 12 * 0.353}mm"), false}
             ],
             [
               {:xmlText, [text: 2, g: 1, svg: 1], 1, [], [26085, 26412, 35486], :text}
             ], [], '', :undeclared}
          ], [], '', :undeclared}
       ], [], '', :undeclared}
    ],
    :xmerl_xml
  )
)

{:ok, {_, contents}} = StringIO.close(pid)
contents
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm"/><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>

これは Erlang の record が惡過ぎて書けない。

これらは全て標準 library だ。標準 library は優れたものである必要が有る。でなければ含まれてゐないはうが好い。

さて PyPI から適切なものを見繕って來れば好いのだらうが、手元で濟ませてしまおう。

from contextlib import contextmanager
from functools import partial
import typing as t
import xml.etree.ElementTree as ET

@contextmanager
def e(
    tag: str, attrib: t.Dict[str, str] = {}, text: str = "", parent: ET.Element = None
) -> None:
    if parent is not None:
        element = ET.SubElement(parent, tag, attrib)
    else:
        element = ET.Element(tag, attrib)
    if text != "":
        element.text = text
    yield partial(e, parent=element)

これでこう成る。

svg = ET.Element(
    "svg",
    {
        "xmlns": "http://www.w3.org/2000/svg",
        "height": "148mm",
        "width": "210mm",
    },
)
with e("g", {}, parent=svg) as _e:
    with _e(
        "line",
        {
            "stroke": "black",
            "x1": "5mm",
            "x2": "200mm",
            "y1": "4mm",
            "y2": "144mm",
        },
    ):
        pass
    with _e(
        "text",
        {
            "fill": "black",
            "font-family": "sans-serif",
            "font-size": "12pt",
            "x": "25mm",
            "y": f"{25 + 12 * 0.353}mm",
        },
        "日本語",
    ):
        pass
ET.tostring(svg).decode("utf-8")
<svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g /><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm" /><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">&#26085;&#26412;&#35486;</text></g></svg>

XML での表現とそれを生成する code での表現とが一致してゐると好い。