UNPKG

@placemarkio/tokml

Version:
1,011 lines (902 loc) 24.1 kB
'use strict'; /** * @typedef {import('unist').Node} Node */ /** * @typedef {Array<Node> | string} ChildrenOrValue * List to use as `children` or value to use as `value`. * * @typedef {Record<string, unknown>} Props * Other fields to add to the node. */ /** * Build a node. * * @template {string} T * @template {Props} P * @template {Array<Node>} C * * @overload * @param {T} type * @returns {{type: T}} * * @overload * @param {T} type * @param {P} props * @returns {{type: T} & P} * * @overload * @param {T} type * @param {string} value * @returns {{type: T, value: string}} * * @overload * @param {T} type * @param {P} props * @param {string} value * @returns {{type: T, value: string} & P} * * @overload * @param {T} type * @param {C} children * @returns {{type: T, children: C}} * * @overload * @param {T} type * @param {P} props * @param {C} children * @returns {{type: T, children: C} & P} * * @param {string} type * Node type. * @param {ChildrenOrValue | Props | null | undefined} [props] * Fields assigned to node (default: `undefined`). * @param {ChildrenOrValue | null | undefined} [value] * Children of node or value of `node` (cast to string). * @returns {Node} * Built node. */ function u(type, props, value) { /** @type {Node} */ const node = {type: String(type)}; if ( (value === undefined || value === null) && (typeof props === 'string' || Array.isArray(props)) ) { value = props; } else { Object.assign(node, props); } if (Array.isArray(value)) { // @ts-expect-error: create a parent. node.children = value; } else if (value !== undefined && value !== null) { // @ts-expect-error: create a literal. node.value = String(value); } return node } /** * @typedef CoreOptions * @property {ReadonlyArray<string>} [subset=[]] * Whether to only escape the given subset of characters. * @property {boolean} [escapeOnly=false] * Whether to only escape possibly dangerous characters. * Those characters are `"`, `&`, `'`, `<`, `>`, and `` ` ``. * * @typedef FormatOptions * @property {(code: number, next: number, options: CoreWithFormatOptions) => string} format * Format strategy. * * @typedef {CoreOptions & FormatOptions & import('./util/format-smart.js').FormatSmartOptions} CoreWithFormatOptions */ const defaultSubsetRegex = /["&'<>`]/g; const surrogatePairsRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; const controlCharactersRegex = // eslint-disable-next-line no-control-regex, unicorn/no-hex-escape /[\x01-\t\v\f\x0E-\x1F\x7F\x81\x8D\x8F\x90\x9D\xA0-\uFFFF]/g; const regexEscapeRegex = /[|\\{}()[\]^$+*?.]/g; /** @type {WeakMap<ReadonlyArray<string>, RegExp>} */ const subsetToRegexCache = new WeakMap(); /** * Encode certain characters in `value`. * * @param {string} value * @param {CoreWithFormatOptions} options * @returns {string} */ function core(value, options) { value = value.replace( options.subset ? charactersToExpressionCached(options.subset) : defaultSubsetRegex, basic ); if (options.subset || options.escapeOnly) { return value } return ( value // Surrogate pairs. .replace(surrogatePairsRegex, surrogate) // BMP control characters (C0 except for LF, CR, SP; DEL; and some more // non-ASCII ones). .replace(controlCharactersRegex, basic) ) /** * @param {string} pair * @param {number} index * @param {string} all */ function surrogate(pair, index, all) { return options.format( (pair.charCodeAt(0) - 0xd800) * 0x400 + pair.charCodeAt(1) - 0xdc00 + 0x10000, all.charCodeAt(index + 2), options ) } /** * @param {string} character * @param {number} index * @param {string} all */ function basic(character, index, all) { return options.format( character.charCodeAt(0), all.charCodeAt(index + 1), options ) } } /** * A wrapper function that caches the result of `charactersToExpression` with a WeakMap. * This can improve performance when tooling calls `charactersToExpression` repeatedly * with the same subset. * * @param {ReadonlyArray<string>} subset * @returns {RegExp} */ function charactersToExpressionCached(subset) { let cached = subsetToRegexCache.get(subset); if (!cached) { cached = charactersToExpression(subset); subsetToRegexCache.set(subset, cached); } return cached } /** * @param {ReadonlyArray<string>} subset * @returns {RegExp} */ function charactersToExpression(subset) { /** @type {Array<string>} */ const groups = []; let index = -1; while (++index < subset.length) { groups.push(subset[index].replace(regexEscapeRegex, '\\$&')); } return new RegExp('(?:' + groups.join('|') + ')', 'g') } /** * The smallest way to encode a character. * * @param {number} code * @returns {string} */ function formatBasic(code) { return '&#x' + code.toString(16).toUpperCase() + ';' } /** * @typedef {import('./core.js').CoreOptions & import('./util/format-smart.js').FormatSmartOptions} Options * @typedef {import('./core.js').CoreOptions} LightOptions */ /** * Encode special characters in `value` as hexadecimals. * * @param {string} value * Value to encode. * @param {LightOptions} [options] * Configuration. * @returns {string} * Encoded value. */ function stringifyEntitiesLight(value, options) { return core(value, Object.assign({format: formatBasic}, options)) } // eslint-disable-next-line no-control-regex -- XO is wrong. const noncharacter = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g; /** * Escape a string. * * @param {string} value * Raw string. * @param {Array<string>} subset * Characters to escape. * @param {RegExp | null | undefined} [unsafe] * Regex to scope `subset` to (optional). * @returns {string} * Escaped string. */ function escape(value, subset, unsafe) { const result = clean(value); return unsafe ? result.replace(unsafe, encode) : encode(result) /** * Actually escape characters. * * @param {string} value * Raw value. * @returns {string} * Copy of `value`, escaped. */ function encode(value) { return stringifyEntitiesLight(value, {subset}) } } /** * Remove non-characters. * * @param {string} value * Raw value. * @returns {string} * Copy of `value` with non-characters removed. */ function clean(value) { return String(value || '').replace(noncharacter, '') } /** * @typedef {import('xast').Cdata} Cdata */ const unsafe$1 = /]]>/g; const subset$3 = ['>']; /** * Serialize a CDATA section. * * @param {Cdata} node * xast cdata node. * @returns {string} * Serialized XML. */ function cdata(node) { return '<![CDATA[' + escape(node.value, subset$3, unsafe$1) + ']]>' } /** * @typedef {import('xast').Comment} Comment */ /** * Serialize a comment. * * @param {Comment} node * xast comment node. * @returns {string} * Serialized XML. */ function comment(node) { return '<!--' + escape(node.value, ['-']) + '-->' } const subset$2 = ['\t', '\n', ' ', '"', '&', "'", '/', '<', '=', '>']; /** * Encode a node name. * * @param {string} value * Raw name. * @returns {string} * Escaped name. */ function name(value) { return escape(value, subset$2) } /** * Count how often a character (or substring) is used in a string. * * @param {string} value * Value to search in. * @param {string} character * Character (or substring) to look for. * @return {number} * Number of times `character` occurred in `value`. */ function ccount(value, character) { const source = String(value); if (typeof character !== 'string') { throw new TypeError('Expected character') } let count = 0; let index = source.indexOf(character); while (index !== -1) { count++; index = source.indexOf(character, index + character.length); } return count } /** * @typedef {import('./index.js').State} State */ /** * Serialize an attribute value. * * @param {string} value * Raw attribute value. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized attribute value. */ function value(value, state) { const result = String(value); let quote = state.options.quote || '"'; if (state.options.quoteSmart) { const other = quote === '"' ? "'" : '"'; if (ccount(result, quote) > ccount(result, other)) { quote = other; } } return quote + escape(result, ['<', '&', quote]) + quote } /** * @typedef {import('xast').Doctype} Doctype * @typedef {import('./index.js').State} State */ /** * Serialize a doctype. * * @param {Doctype} node * xast doctype node. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized XML. */ function doctype(node, state) { const nodeName = name(node.name); const pub = node.public; const sys = node.system; let result = '<!DOCTYPE'; if (nodeName !== '') { result += ' ' + nodeName; } if (pub) { result += ' PUBLIC ' + value(pub, state); } else if (sys) { result += ' SYSTEM'; } if (sys) { result += ' ' + value(sys, state); } return result + '>' } /** * @typedef {import('xast').Element} Element * @typedef {import('./index.js').State} State */ const own$1 = {}.hasOwnProperty; /** * Serialize an element. * * @param {Element} node * xast element node. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized XML. */ function element(node, state) { const nodeName = name(node.name); const content = all(node, state); const attributes = node.attributes || {}; const close = content ? false : state.options.closeEmptyElements; /** @type {Array<string>} */ const attrs = []; /** @type {string} */ let key; for (key in attributes) { if (own$1.call(attributes, key)) { const result = attributes[key]; if (result !== null && result !== undefined) { attrs.push(name(key) + '=' + value(result, state)); } } } return ( '<' + nodeName + (attrs.length === 0 ? '' : ' ' + attrs.join(' ')) + (close ? (state.options.tightClose ? '' : ' ') + '/' : '') + '>' + content + (close ? '' : '</' + nodeName + '>') ) } /** * @typedef {import('xast').Instruction} Instruction */ const unsafe = /\?>/g; const subset$1 = ['>']; /** * Serialize an instruction. * * @param {Instruction} node * xast instruction node. * @returns {string} * Serialized XML. */ function instruction(node) { const nodeName = name(node.name) || 'x'; const result = escape(node.value, subset$1, unsafe); return '<?' + nodeName + (result ? ' ' + result : '') + '?>' } /** * @typedef {import('xast').Text} Text * @typedef {import('../index.js').Raw} Raw */ const subset = ['&', '<']; /** * Serialize a text. * * @param {Raw | Text} node * xast text node (or raw). * @returns {string} * Serialized XML. */ function text(node) { return escape(node.value, subset) } /** * @typedef {import('../index.js').Raw} Raw * @typedef {import('./index.js').State} State */ /** * Serialize a (non-standard) raw. * * @param {Raw} node * xast raw node. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized XML. */ function raw(node, state) { return state.options.allowDangerousXml ? node.value : text(node) } /** * @typedef {import('xast').Nodes} Nodes * @typedef {import('xast').Parents} Parents * @typedef {import('xast').RootContent} RootContent * @typedef {import('./index.js').State} State */ const own = {}.hasOwnProperty; const handlers = { cdata, comment, doctype, element, instruction, raw, root: all, text }; /** * Serialize a node. * * @param {Nodes} node * xast node. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized XML. */ function one(node, state) { const type = node && node.type; if (!type) { throw new Error('Expected node, not `' + node + '`') } if (!own.call(handlers, type)) { throw new Error('Cannot compile unknown node `' + type + '`') } const handle = handlers[type]; // @ts-expect-error hush, node matches `type`. const result = handle(node, state); return result } /** * Serialize all children of `parent`. * * @param {Parents} parent * xast parent node. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized XML. */ function all(parent, state) { /** @type {Array<RootContent>} */ const children = (parent && parent.children) || []; let index = -1; /** @type {Array<string>} */ const results = []; while (++index < children.length) { results[index] = one(children[index], state); } return results.join('') } /** * @typedef {import('xast').Literal} Literal * @typedef {import('xast').Nodes} Nodes */ /** * Serialize a xast tree to XML. * * @param {Array<Nodes> | Nodes} tree * xast node(s) to serialize. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {string} * Serialized XML. */ function toXml(tree, options) { /** @type {State} */ const state = {options: {}}; // Make sure the quote is valid. if ( typeof state.options.quote === 'string' && state.options.quote !== '"' && state.options.quote !== "'" ) { throw new Error( 'Invalid quote `' + state.options.quote + '`, expected `\'` or `"`' ) } /** @type {Nodes} */ // @ts-expect-error Assume no `root` in `node`. const node = Array.isArray(tree) ? {type: 'root', children: tree} : tree; return one(node, state) } /** * @typedef {import('xast').Element} Element * @typedef {import('xast').Nodes} Nodes * @typedef {import('xast').Root} Root */ /** * @typedef {Element | Root} Result * Result from a `x` call. * * @typedef {boolean | number | string | null | undefined} Value * Attribute value * * @typedef {{[attribute: string]: Value}} Attributes * Acceptable value for element properties. * * @typedef {boolean | number | string | null | undefined} PrimitiveChild * Primitive children, either ignored (nullish), or turned into text nodes. * @typedef {Array<Nodes | PrimitiveChild>} ArrayChild * List of children. * @typedef {Nodes | PrimitiveChild | ArrayChild} Child * Acceptable child value. */ // Define JSX. /** * @typedef {import('./jsx-classic.js').Element} x.JSX.Element * @typedef {import('./jsx-classic.js').IntrinsicAttributes} x.JSX.IntrinsicAttributes * @typedef {import('./jsx-classic.js').IntrinsicElements} x.JSX.IntrinsicElements * @typedef {import('./jsx-classic.js').ElementChildrenAttribute} x.JSX.ElementChildrenAttribute */ /** * Create XML trees in xast. * * @param name * Qualified name. * * Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). * When string, an `Element` is built. * When nullish, a `Root` is built instead. * @param attributes * Attributes of the element or first child. * @param children * Children of the node. * @returns * `Element` or `Root`. */ const x = // Note: not yet possible to use the spread `...children` in JSDoc overloads. /** * @type {{ * (): Root * (name: null | undefined, ...children: Array<Child>): Root * (name: string, attributes?: Attributes, ...children: Array<Child>): Element * (name: string, ...children: Array<Child>): Element * }} */ ( /** * @param {string | null | undefined} [name] * @param {Attributes | Child | null | undefined} [attributes] * @param {Array<Child>} children * @returns {Result} */ function (name, attributes, ...children) { let index = -1; /** @type {Result} */ let node; if (name === undefined || name === null) { node = {type: 'root', children: []}; // @ts-expect-error: Root builder doesn’t accept attributes. children.unshift(attributes); } else if (typeof name === 'string') { node = {type: 'element', name, attributes: {}, children: []}; if (isAttributes(attributes)) { /** @type {string} */ let key; for (key in attributes) { // Ignore nullish and NaN values. if ( attributes[key] !== undefined && attributes[key] !== null && (typeof attributes[key] !== 'number' || !Number.isNaN(attributes[key])) ) { node.attributes[key] = String(attributes[key]); } } } else { children.unshift(attributes); } } else { throw new TypeError('Expected element name, got `' + name + '`') } // Handle children. while (++index < children.length) { addChild(node.children, children[index]); } return node } ); /** * Add children. * * @param {Array<Child>} nodes * List of nodes. * @param {Child} value * Child. * @returns {undefined} * Nothing. */ function addChild(nodes, value) { let index = -1; if (value === undefined || value === null) ; else if (typeof value === 'string' || typeof value === 'number') { nodes.push({type: 'text', value: String(value)}); } else if (Array.isArray(value)) { while (++index < value.length) { addChild(nodes, value[index]); } } else if (typeof value === 'object' && 'type' in value) { if (value.type === 'root') { addChild(nodes, value.children); } else { nodes.push(value); } } else { throw new TypeError('Expected node, nodes, string, got `' + value + '`') } } /** * Check if `value` is `Attributes`. * * @param {Attributes | Child} value * Value. * @returns {value is Attributes} * Whether `value` is `Attributes`. */ function isAttributes(value) { if ( value === null || value === undefined || typeof value !== 'object' || Array.isArray(value) ) { return false } return true } const BR = u("text", "\n"); const TAB = u("text", " "); /** * Convert nested folder structure to KML. This expects * input that follows the same patterns as [toGeoJSON](https://github.com/placemark/togeojson)'s * kmlWithFolders method: a tree of folders and features, * starting with a root element. */ function foldersToKML(root) { return toXml(u("root", [ x("kml", { xmlns: "http://www.opengis.net/kml/2.2" }, x("Document", root.children.flatMap((child) => convertChild(child)))), ])); } /** * Convert a GeoJSON FeatureCollection to a string of * KML data. */ function toKML(featureCollection) { return toXml(u("root", [ x("kml", { xmlns: "http://www.opengis.net/kml/2.2" }, x("Document", featureCollection.features.flatMap((feature) => convertFeature(feature)))), ])); } function convertChild(child) { switch (child.type) { case "Feature": return convertFeature(child); case "folder": return convertFolder(child); } } function convertFolder(folder) { const id = ["string", "number"].includes(typeof folder.meta.id) ? { id: String(folder.meta.id), } : {}; return [ BR, x("Folder", id, [ BR, ...folderMeta(folder.meta), BR, TAB, ...folder.children.flatMap((child) => convertChild(child)), ]), ]; } const META_PROPERTIES = [ "address", "description", "name", "open", "visibility", "phoneNumber", ]; function folderMeta(meta) { return META_PROPERTIES.filter((p) => meta[p] !== undefined).map((p) => { return x(p, [u("text", String(meta[p]))]); }); } function convertFeature(feature) { const { id } = feature; const idMember = ["string", "number"].includes(typeof id) ? { id: id, } : {}; return [ BR, x("Placemark", idMember, [ BR, ...propertiesToTags(feature.properties), BR, TAB, ...(feature.geometry ? [convertGeometry(feature.geometry)] : []), ]), ]; } function join(position) { return `${position[0]},${position[1]}`; } function coord1(coordinates) { return x("coordinates", [u("text", join(coordinates))]); } function coord2(coordinates) { return x("coordinates", [u("text", coordinates.map(join).join("\n"))]); } function valueToString(value) { switch (typeof value) { case "string": { return value; } case "boolean": case "number": { return String(value); } case "object": { try { return JSON.stringify(value); } catch (_e) { return ""; } } } return ""; } function maybeCData(value) { if (value && typeof value === "object" && "@type" in value && value["@type"] === "html" && "value" in value && typeof value.value === "string") { return u("cdata", value.value); } return valueToString(value); } function propertiesToTags(properties) { if (!properties) return []; const { name, description, visibility, ...otherProperties } = properties; return [ name && x("name", [u("text", valueToString(name))]), description && x("description", [u("text", maybeCData(description))]), visibility !== undefined && x("visibility", [u("text", visibility ? "1" : "0")]), x("ExtendedData", Object.entries(otherProperties).flatMap(([name, value]) => [ BR, TAB, x("Data", { name: name }, [ x("value", [ u("text", typeof value === "string" ? value : JSON.stringify(value)), ]), ]), ])), ].filter(Boolean); } const linearRing = (ring) => x("LinearRing", [coord2(ring)]); function convertMultiPoint(geometry) { return x("MultiGeometry", geometry.coordinates.flatMap((coordinates) => [ BR, convertGeometry({ type: "Point", coordinates, }), ])); } function convertMultiLineString(geometry) { return x("MultiGeometry", geometry.coordinates.flatMap((coordinates) => [ BR, convertGeometry({ type: "LineString", coordinates, }), ])); } function convertMultiPolygon(geometry) { return x("MultiGeometry", geometry.coordinates.flatMap((coordinates) => [ BR, convertGeometry({ type: "Polygon", coordinates, }), ])); } function convertPolygon(geometry) { const [outerBoundary, ...innerRings] = geometry.coordinates; return x("Polygon", [ BR, x("outerBoundaryIs", [BR, TAB, linearRing(outerBoundary)]), ...innerRings.flatMap((innerRing) => [ BR, x("innerBoundaryIs", [BR, TAB, linearRing(innerRing)]), ]), ]); } function convertGeometry(geometry) { switch (geometry.type) { case "Point": return x("Point", [coord1(geometry.coordinates)]); case "MultiPoint": return convertMultiPoint(geometry); case "LineString": return x("LineString", [coord2(geometry.coordinates)]); case "MultiLineString": return convertMultiLineString(geometry); case "Polygon": return convertPolygon(geometry); case "MultiPolygon": return convertMultiPolygon(geometry); case "GeometryCollection": return x("MultiGeometry", geometry.geometries.flatMap((geometry) => [ BR, convertGeometry(geometry), ])); } } exports.foldersToKML = foldersToKML; exports.toKML = toKML; //# sourceMappingURL=tokml.cjs.map