#!/usr/bin/env python3
# encoding: utf-8
# api: python
##type: cli
# category: convert
# title: plugin meta to .inx
# description: Convert plugin description to Inkscape .inx
# license: PD
# version: 0.7
# state: beta
# depends: pluginconf (>= 0.7.6)
# config: -
# pylint: disable=missing-docstring, invalid-name, no-else-return
# doc: https://gitlab.com/inkscape/extensions/-/blob/master/inkscape.extension.rng
#
# Quickly converts a PMD comment block to .inx descriptor.
# Mapping some types and modes.
#
# * Needs manual { xml: </injection> } for closing notebooks/pages.
# * Introduces type=label for inserting a <label>
# * Outputs a sample add_argument() list as well.
#
import sys
import re
import pluginconf
meta = {}
xml_template = """<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>{name}</name>
<description>{desc}</description>
<!--<schema:softwareVersion xmlns:schema="https://schema.org/">{version}</schema:softwareVersion>-->
<category>{submenu}</category>
<id>{id}</id>
<dependency type="executable" location="inx">{fn}</dependency>
<label appearance="header">{header}</label>
{params}
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="{submenu}" />
</effects-menu>
<menu-tip>{desc}</menu-tip>
</effect>
{export}
<script>
<command location="inx" interpreter="python">{fn}</command>
</script>
<!--
{args}
-->
</inkscape-extension>
"""
map = {
"type": {
"select": "select",
"color": "color",
"path": "path",
"int": "int",
"float": "float",
"range": "int",
"bool": "bool",
"file": "path",
"str": "string",
"text": "string",
"tab": "notebook",
"group": "optiongroup",
"label": "label",
"notebook": "notebook",
},
"mode": {
"bool": "checkbox",
"color": "color",
"range": "slider",
"float": "float",
},
"appearance": {
"text": "multiline",
"range": "full",
"enum": "combo",
"select": "combo",
},
"translatable": {
"0": "no",
"1": "yes",
},
"arg": {
"string": "str",
"str": "str",
"int": "int",
"float": "float",
"bool": "inkex.Boolean",
"color": "inkex.Color",
}
}
bool_true_match = re.compile(r"^(1|yes|true)", re.I).match
notebook = None
class new_notebook:
""" nestable notebook pages / tabs """
tab = ""
param = ""
prev = None
curr = None
def __init__(self, param, o):
self.param = param
self.nest("", o)
self.curr = self
self.prev = notebook
def nest(self, name, o):
if name == self.tab:
return
if self.tab:
yield from self.end(name)
if name:
yield f"""
<page name="{name}" gui-text="{o.get("tab_desc") or name}">
"""
self.tab = name
def end(self, name, all=True):
yield """</page>"""
if self.param and not name:
self.param = ""
yield """</param>"""
notebook = self.prev
if all and self.prev:
self.prev.end(name, all)
def enum_item(k, v, _add="", xml="option"):
return f"""<{xml} {_add} value="{k}">{v}</{xml}>"""
def param(o):
global notebook
# arguments
raw_type = o.get("type")
_type = raw_type or "str"
_type = map["type"].get(_type, "string")
_name = o.get("name", "_0")
_val = o.get("value", "")
_desc = o.get("description", "…")
_tab = o.get("category") or o.get("class") or o.get("page")
# optional attributes via {_add}
_add = ""
for prop in "mode", "appearance", "translatable":
if map[prop].get(raw_type) and not o.get(prop):
o[prop] = map[prop][raw_type]
if o.get(prop):
_add = _add + f"{prop}=\"{o[prop]}\" "
if o.get("help"):
_add += f"gui-description=\"{o['help']}\""
if bool_true_match(o.get("hidden", "")):
_add += 'gui-hidden="true" '
# value mapping
if _type == "bool":
_val = "true" if bool_true_match( _val) else "false"
# raw output
if notebook:
yield from notebook.nest(_tab, o)
if "xml" in o:
yield o["xml"]
elif "label" in o or _type == "label":
text = o.get("label") or _desc
if re.match(r"^[#{$]", text):
text = meta[re.sub(r"^[#{$]+|[:}$]*$", "", text)] # in-place reference
yield f"""
<label {_add}>{text}</label>
"""
# variants
elif _type == "select":
_option_add = 'translatable="no" ' if not bool_true_match(o.get("translatable", "yes")) else ''
ls = "\n ".join([enum_item(k, v, _option_add) for k, v in o.get("select", {}).items()])
yield f"""
<param name="{_name}" type="optiongroup" {_add} gui-text="{_desc}">
{ls}
</param>
"""
elif _type == "notebook":
notebook = new_notebook(_name, o)
yield f"""
<param name="{_name}" type="{_type}" {_add} gui-text="{_desc}">
"""
elif _type in ("int", "float"):
yield f"""
<param name="{_name}" type="{_type}" min="{o.get("min",0)}" max="{o.get("max",100)}" precision="{o.get("precision",1)}" {_add} gui-text="{_desc}">{_val}</param>
"""
else:
yield f"""
<param name="{_name}" type="{_type}" {_add} gui-text="{_desc}">{_val}</param>
"""
def inx_export(m, xml="output"):
if not "inx_export" in m:
return ""
_ext, _mime, _type, _tip = re.split(r"\s*,\s*", m["inx_export"])
return f"""
<!--{xml}>
<extension>{_ext}</extension>
<mimetype>{_mime}</mimetype>
<filetypename>{_type}</filetypename>
<filetypetooltip>{_tip}</filetypetooltip>
<dataloss>true</dataloss>
</{xml}-->
"""
def add_arg(o):
if not o.get("name"):
return ""
_type = map["arg"].get(o["type"], "str")
_name = o.get("name")
_val = o.get("value", "")
if _type == "inkex.Boolean":
_val = "True" if bool_true_match(_val) else "False"
elif _type not in ("int", "float"):
_val = '"' + _val + '"'
return f""" pars.add_argument("-"+"-{_name}", type={_type}, dest="{_name}", default={_val}, help="{o.get("description","")}")"""
def generate_inx(m):
""" roughly convert builtin plugin description to .inx """
params = []
for o in m["config"]:
for line in param(o):
params.append(line)
if notebook.curr:
params = params + list(notebook.end("", True))
args = list(
filter(
None,
[add_arg(o) for o in m["config"] if o["type"] != "label"]
)
)
return xml_template.format(
name=m.get("title", "Unnamed"),
desc=m.get("description", "extension script"),
version=m.get("version"),
id=m["id"],
header=m.get("description", "random plugin"),
params="\n ".join(params),
submenu=m.get("category", "Else").title(),
export=inx_export(m),
fn=re.sub(".+/", "", m["fn"]),
args="\n".join(args)
)
if len(sys.argv) == 2:
meta = pluginconf.plugin_meta(fn=sys.argv[1], max_length=8192)
print(
generate_inx(
meta
)
)
else:
print("Syntax: pmd2inx script.py")