Skip to content

Commit

Permalink
feat(examples): extend xml-converter, add CLI version, update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Sep 27, 2018
1 parent 2f01447 commit 95ba4f6
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 194 deletions.
34 changes: 34 additions & 0 deletions assets/dot/xml-converter.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
digraph g {
rankdir=LR;
node[fontname="Inconsolata", fontsize="11"];
edge[arrowsize="0.75", fontname="Inconsolata", fontsize="9"];

rank=same {
prettyPrint;
doubleQuote;
trailingComma;
removeTags;
removeAttribs;
xml;
}

prettyPrint -> formatOpts;
doubleQuote -> formatOpts;
trailingComma -> formatOpts;

removeTags -> opts;
removeAttribs -> opts;
formatOpts -> opts;

xml -> main;
opts -> main;

main -> app -> ui;

ui -> xml[label="user", style=dashed];
ui -> prettyPrint[label="user", style=dashed];
ui -> doubleQuote[label="user", style=dashed];
ui -> trailingComma[label="user", style=dashed];
ui -> removeTags[label="user", style=dashed];
ui -> removeAttribs[label="user", style=dashed];
}
Binary file added assets/xml-converter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ If you want to [contribute](../CONTRIBUTING.md) an example, please get in touch
| 29 | [transducers-hdom](./transducers-hdom) | Transducer & rstream based hdom UI updates | hdom, rstream, transducers-hdom | basic |
| 30 | [triple-query](./triple-query) | Triple store query results & sortable table | atom, hdom, hdom-components, rstream-query, transducers | intermediate |
| 31 | [webgl](./webgl) | Canvas component handling | hdom, hdom-components | basic |
| 32 | [xml-converter](./xml-converter) | XML/HTML/SVG to hiccup conversion as you type | rstream, sax, transducers, transducers-hdom | basic |
| 32 | [xml-converter](./xml-converter) | XML/HTML/SVG to hiccup conversion as you type | rstream, sax, transducers, transducers-hdom | advanced |
72 changes: 68 additions & 4 deletions examples/xml-converter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,83 @@

This example uses
[@thi.ng/sax](https://github.com/thi-ng/umbrella/tree/master/packages/sax)
to convert XML/HTML syntax into
[@thi.ng/hiccup](https://github.com/thi-ng/umbrella/tree/master/packages/hiccup)
syntax.
to convert XML/HTML/SVG syntax into
[hiccup](https://github.com/thi-ng/umbrella/tree/master/packages/hiccup)
/ JSON syntax and provides several options to filter the parsed tree and
control the resulting output format.

TODO better output formatting, html boolean attribs
## Dataflow

This diagram illustrates the
[@thi.ng/rstream](https://github.com/thi-ng/umbrella/tree/master/packages/rstream)
dataflow topology used by the browser app:

![dataflow](https://raw.githubusercontent.com/thi-ng/umbrella/master/assets/xml-converter.png)

## Browser version
```bash
git clone https://github.com/thi-ng/umbrella.git
cd umbrella/examples/xml-converter
yarn install
yarn start
```

## CLI version

In addition to the above browser UI, this example can be built as a
**basic** CLI tool to convert & filter files (the result is always
written to stdout).

```bash
# in this example's project root...
yarn build-cli

bin/hiccup --help
# Usage: hiccup [options] <file>
#
# Options:
#
# -V, --version output the version number
# -t, --tags <items> remove tags from tree
# -a, --attribs <items> remove attribs from tree
# -v, --var <name> generate TS export var declaration
# -s, --single-quote use single quotes
# -p, --no-pretty disable pretty printing
# -h, --help output usage information

# Example file
cat << EOF > foo.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px"
height="10px"
viewBox="0 0 10 10"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<title>add</title>
<desc>Foo bar</desc>
<defs></defs>
<g id="add" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M6,4 L10,4 L10,6 L6,6 L6,10 L4,10 L4,6 L0,6 L0,4 L4,4 L4,0 L6,0 L6,4 Z"
id="Plus" fill="#000000"/>
</g>
</svg>
EOF

bin/hiccup \
-v ICON \ # create a target var name
-t title,desc,defs \ # remove tags
-a id,style,stroke,fill,xmlns,width,height,version \ # remove attribs
foo.svg
# export const ICON =
# ["svg", { viewBox: "0 0 10 10" },
# ["g",
# {
# "fill-rule": "evenodd",
# "stroke-width": 1,
# },
# ["path", { d: "M6,4 L10,4 L10,6 L6,6 L6,10 L4,10 L4,6 L0,6 L0,4 L4,4 L4,0 L6,0 L6,4 Z" }]]];
```

## Authors

- Karsten Schmidt
Expand Down
43 changes: 43 additions & 0 deletions examples/xml-converter/bin/hiccup
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const program = require("commander");
const package = require("../package.json");
const convert = require("./convert");
const format = require("./format");
const utils = require("./utils");

program
.version(package.version)
.usage("[options] <file>")
.option("-t, --tags <items>", "remove tags from tree", utils.asSet)
.option("-a, --attribs <items>", "remove attribs from tree", utils.asSet)
.option("-v, --var <name>", "generate TS export var decl")
.option("-s, --single-quote", "use single quotes")
.option("-p, --no-pretty", "disable pretty printing")
.parse(process.argv);

if (program.args.length == 0) {
console.log("Please run: hiccup --help");
process.exit(1);
}

const xmlFile = path.resolve(program.args[0]);
const quote = program.singleQuote ? `'` : `"`;
const copts = {
format: program.pretty ?
{ ...format.DEFAULT_FORMAT, quote, indent: 4 } :
{ ...format.COMPACT_FORMAT, quote },
removeAttribs: program.attribs || new Set(),
removeTags: program.tags || new Set(),
};

const xml = fs.readFileSync(xmlFile).toString();
const hiccup = convert.convertXML(xml, copts);

console.log(
program.var ?
`export const ${program.var} =\n${hiccup};` :
hiccup
);
11 changes: 0 additions & 11 deletions examples/xml-converter/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,13 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>xml-converter</title>
<link href="https://unpkg.com/tachyons@4.9.1/css/tachyons.min.css" rel="stylesheet" />
<!-- <link href="tachyons.min.css" rel="stylesheet" /> -->
<style>
</style>
</head>

<body class="ma0 pa2 sans-serif">
<div id="app"></div>
<div>
<h3>Current features</h3>
<ul>
<li>HTML boolean attribs</li>
<li>Numeric & boolean attrib value parsing</li>
<li><code>style</code> attrib conversion</li>
<li>Standard HTML entity unescaping</li>
<li>Input must be well formed XML</li>
<li>Hiccup specific (more compact) JSON formatting</li>
<li>See issue <a class="link blue" href="https://github.com/thi-ng/umbrella/issues/48">#48</a></li>
</ul>
<a class="link blue" href="https://github.com/thi-ng/umbrella/tree/master/examples/xml-converter">Source code</a>
</div>
<script type="text/javascript" src="./src/index.ts"></script>
Expand Down
4 changes: 3 additions & 1 deletion examples/xml-converter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"clean": "rm -rf .cache build out",
"build": "yarn clean && parcel build index.html -d out --public-url ./ --no-source-maps --no-cache --detailed-report",
"build-cli": "tsc -p tsconfig-cli.json",
"start": "parcel index.html -p 8080 --open"
},
"devDependencies": {
Expand All @@ -18,7 +19,8 @@
"@thi.ng/rstream": "latest",
"@thi.ng/sax": "latest",
"@thi.ng/strings": "latest",
"@thi.ng/transducers-hdom": "latest"
"@thi.ng/transducers-hdom": "latest",
"commander": "^2.18.0"
},
"browserslist": [
"last 3 Chrome versions"
Expand Down
137 changes: 137 additions & 0 deletions examples/xml-converter/src/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { isString } from "@thi.ng/checks/is-string";
import {
parse,
ParseElement,
ParseEvent,
Type
} from "@thi.ng/sax";
import { comp } from "@thi.ng/transducers/func/comp";
import { pairs } from "@thi.ng/transducers/iter/pairs";
import { assocObj } from "@thi.ng/transducers/rfn/assoc-obj";
import { last } from "@thi.ng/transducers/rfn/last";
import { push } from "@thi.ng/transducers/rfn/push";
import { transduce } from "@thi.ng/transducers/transduce";
import { filter } from "@thi.ng/transducers/xform/filter";
import { map } from "@thi.ng/transducers/xform/map";
import { DEFAULT_FORMAT, format, FormatOpts } from "./format";

export interface ConversionOpts {
format: FormatOpts;
removeTags: Set<string>;
removeAttribs: Set<string>;
}

export const DEFAULT_OPTS: ConversionOpts = {
format: DEFAULT_FORMAT,
removeAttribs: new Set(),
removeTags: new Set(),
};

// converts given XMLish string into formatted hiccup
export const convertXML = (src: string, opts: Partial<ConversionOpts> = {}) => {
let tree = transformTree(
parseXML(src),
<ConversionOpts>{ ...DEFAULT_OPTS, ...opts }
);
return format({ ...DEFAULT_FORMAT, ...opts.format }, "", tree);
};

// parses given XMLish string using @thi.ng/sax transducer into a
// sequence of parse events. we only care about the final (or error)
// event, which will be related to the final close tag and contains the
// entire tree
const parseXML = (src: string) =>
transduce(
comp(
parse({ trim: true, boolean: true, entities: true }),
filter((e) => e.type === Type.ELEM_END || e.type === Type.ERROR)
),
last(),
src
);

// transforms string of CSS properties into a plain object
const transformCSS = (css: string) =>
css.split(";").reduce(
(acc, p) => {
const [k, v] = p.split(":");
(v != null) && (acc[k.trim()] = parseAttrib([k, v.trim()])[1]);
return acc;
},
{}
);

// takes attrib key-value pair and attempts to coerce / transform its
// value. returns updated pair.
const parseAttrib = (attrib: string[]) => {
let [k, v] = attrib;
if (isString(v)) {
v = v.replace(/[\n\r]+\s*/g, " ");
return k === "style" ?
[k, transformCSS(v)] :
v === "true" ?
[k, true] :
v === "false" ?
[k, false] :
[k, /^[0-9.e+-]+$/.test(v) ?
parseFloat(v) :
v];
}
return attrib;
};

// transforms an entire object of attributes
const transformAttribs = (attribs: any, remove: Set<string> = new Set()) =>
transduce(
comp(
filter((a) => !remove.has(a[0])),
map(parseAttrib)
),
assocObj(),
{},
pairs<string>(attribs)
);

// transforms element name by attempting to form Emmet-like tags
const transformTag = (tag: string, attribs: any) => {
if (attribs.id) {
tag += "#" + attribs.id;
delete attribs.id;
}
if (isString(attribs.class)) {
const classes = attribs.class.replace(/\s+/g, ".");
classes.length && (tag += "." + classes);
delete attribs.class;
}
return tag;
};

// recursively transforms entire parse tree
const transformTree = (tree: ParseEvent | ParseElement, opts: ConversionOpts) => {
if ((<ParseEvent>tree).type === Type.ERROR) {
return ["error", tree.body];
}
if (opts.removeTags.has(tree.tag)) {
return;
}
const attribs = transformAttribs(tree.attribs, opts.removeAttribs);
const res: any[] = [transformTag(tree.tag, attribs)];
if (Object.keys(attribs).length) {
res.push(attribs);
}
if (tree.body) {
res.push(tree.body);
}
if (tree.children && tree.children.length) {
transduce(
comp(
map((t: any) => transformTree(t, opts)),
filter((t) => !!t)
),
push(),
res,
tree.children
)
}
return res;
};
Loading

0 comments on commit 95ba4f6

Please sign in to comment.