I would tend to think of it in two stages:
- XML -> JSON
- JSON querying and calculation
There are various approaches to stage one (web search should yield some options).
Stage two, generating a report from some JSON data can also be done in several ways:
- People like
jq
a lot - You could do it with Keyboard Maestro
%JSONValue%
tags (see below), and some CALC functions.
(If you want to skip the JSON stage, XQuery
is always worth experimenting with)
Reading from XML to JSON.kmmacros (11 KB)
Expand disclosure triangle to view JS source
(() => {
"use strict";
// unwrap :: NSObject -> a
const unWrap = ObjC.unwrap;
const main = () =>
either(
alert("JSON from XML")
)(
x => x
)(
bindLR(
readFileLR(
Application("Keyboard Maestro Engine")
.getvariable("xmlFilePath")
)
)(
compose(
fmapLR(
compose(
x => x.nest[0].nest[0].nest[0]
.nest.map(dict => dict.root),
xmlNodeDict
)
),
xmlNodeFromXmlStringLR
)
)
);
// xmlNodeDict :: NSXMLNode -> Node Dict
const xmlNodeDict = xmlNode => {
const
blnChiln = 0 < parseInt(
xmlNode.childCount, 10
);
return Node({
name: unWrap(xmlNode.name),
content: blnChiln ? (
undefined
) : (unWrap(xmlNode.stringValue) || " "),
attributes: (() => {
const attrs = unWrap(xmlNode.attributes);
return Array.isArray(attrs) ? (
attrs.reduce(
(a, x) => Object.assign(a, {
[unWrap(x.name)]: unWrap(
x.stringValue
)
}),
{}
)
) : {};
})()
})(
blnChiln ? (
unWrap(xmlNode.children)
.reduce(
(a, x) => a.concat(xmlNodeDict(x)),
[]
)
) : []
);
};
// ----------------------- JXA -----------------------
// alert :: String => String -> IO String
const alert = title =>
s => {
const sa = Object.assign(
Application("System Events"), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ["OK"],
defaultButton: "OK"
}),
s
);
};
// readFileLR :: FilePath -> Either String IO String
const readFileLR = fp => {
// Either a message or the contents of any
// text file at the given filepath.
const
e = $(),
ns = $.NSString
.stringWithContentsOfFileEncodingError(
$(fp).stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
);
return ns.isNil() ? (
Left(ObjC.unwrap(e.localizedDescription))
) : Right(ObjC.unwrap(ns));
};
// xmlNodeFromXmlStringLR :: XML String ->
// Either String NSXMLNode
const xmlNodeFromXmlStringLR = s => {
const
error = $(),
node = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
s, 0, error
);
return node.isNil() ? (() => {
const
problem = ObjC.unwrap(
error.localizedDescription
);
return Left(
`Not parseable as XML:\n\n${problem}`
);
})() : Right(node);
};
// --------------------- GENERIC ---------------------
// Left :: a -> Either a b
const Left = x => ({
type: "Either",
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: "Node",
root: v,
nest: xs || []
});
// Right :: b -> Either a b
const Right = x => ({
type: "Either",
Right: x
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => m.Left ? (
m
) : mf(m.Right);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => x
);
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
const fmapLR = f =>
// Either f mapped into the contents of any Right
// value in e, or e unchanged if is a Left value.
e => "Left" in e ? (
e
) : Right(f(e.Right));
// --------------------- LOGGING ---------------------
// sj :: a -> String
const sj = (...args) =>
// Abbreviation of showJSON for quick testing.
// Default indent size is two, which can be
// overriden by any integer supplied as the
// first argument of more than one.
JSON.stringify.apply(
null,
1 < args.length && !isNaN(args[0]) ? [
args[1], null, args[0]
] : [args[0], null, 2]
);
return sj(main());
})();