diff options
Diffstat (limited to 'generate-elm.js')
-rw-r--r-- | generate-elm.js | 325 |
1 files changed, 213 insertions, 112 deletions
diff --git a/generate-elm.js b/generate-elm.js index 1643428..7207f4e 100644 --- a/generate-elm.js +++ b/generate-elm.js @@ -5,39 +5,44 @@ function generateProperties(spec) { var docs = {}; var enums = {}; layouts.forEach(l => { - const layerType = titleCase(l.split('_')[1]) + const layerType = titleCase(l.split("_")[1]); docs[layerType] = []; codes[layerType] = []; Object.entries(spec[l]).forEach(([name, prop]) => { - if (name == 'visibility') return ''; - if (prop.type === 'enum') { - enums[name] = Object.keys(prop.values).join(' | '); + if (name == "visibility") return ""; + if (prop.type === "enum") { + enums[name] = Object.keys(prop.values).join(" | "); } - codes[layerType].push(generateElmProperty(name, prop, layerType, 'Layout')); + codes[layerType].push( + generateElmProperty(name, prop, layerType, "Layout") + ); docs[layerType].push(camelCase(name)); - }) - }) + }); + }); paints.forEach(l => { - const layerType = titleCase(l.split('_')[1]) + const layerType = titleCase(l.split("_")[1]); Object.entries(spec[l]).forEach(([name, prop]) => { - if (name == 'visibility') return ''; - if (prop.type === 'enum') { - enums[name] = Object.keys(prop.values).join(' | '); + if (name == "visibility") return ""; + if (prop.type === "enum") { + enums[name] = Object.keys(prop.values).join(" | "); } - codes[layerType].push(generateElmProperty(name, prop, layerType, 'Paint')) + codes[layerType].push( + generateElmProperty(name, prop, layerType, "Paint") + ); docs[layerType].push(camelCase(name)); - }) - }) - Object.values(docs).forEach(d => d.sort()) + }); + }); + Object.values(docs).forEach(d => d.sort()); Object.values(codes).forEach(d => d.sort()); - console.log(enums); return ` module Mapbox.Layer exposing ( Layer, SourceId, Background, Fill, Symbol, Line, Raster, Circle, FillExtrusion, Heatmap, Hillshade, LayerAttr, encode, background, fill, symbol, line, raster, circle, fillExtrusion, heatmap, hillshade, - metadata, source, sourceLayer, minzoom, maxzoom, filter, visible, - ${Object.values(docs).map(d => d.join(', ')).join(',\n ')}) + metadata, sourceLayer, minzoom, maxzoom, filter, visible, + ${Object.values(docs) + .map(d => d.join(", ")) + .join(",\n ")}) {-| Layers specify what is actually rendered on the map and are rendered in order. @@ -62,15 +67,19 @@ Paint properties are applied later in the rendering process. Changes to a paint ### General Attributes @docs LayerAttr -@docs metadata, source, sourceLayer, minzoom, maxzoom, filter, visible - -${Object.entries(docs).map(([section, docs]) => `### ${section} Attributes\n\n@docs ${docs.join(', ')}`).join('\n\n')} +@docs metadata, sourceLayer, minzoom, maxzoom, filter, visible + +${Object.entries(docs) + .map( + ([section, docs]) => + `### ${section} Attributes\n\n@docs ${docs.join(", ")}` + ) + .join("\n\n")} -} import Array exposing (Array) -import Json.Decode import Json.Encode as Encode exposing (Value) -import Mapbox.Expression as Expression exposing (Anchor, CameraExpression, Color, DataExpression, Expression, LineJoin) +import Mapbox.Expression as Expression exposing (Anchor, Auto, CameraExpression, Color, DataExpression, Expression, LineCap, LineJoin, Position, RasterResampling, SymbolPlacement, TextFit, TextJustify, TextTransform, FormattedText) {-| Represents a layer. -} type Layer @@ -121,9 +130,7 @@ encode (Layer value) = value - - -layerImpl tipe source id attrs = +layerImpl tipe id source attrs = [ ( "type", Encode.string tipe ) , ( "id", Encode.string id ) , ( "source", Encode.string source) @@ -137,16 +144,16 @@ encodeAttrs attrs = let { top, layout, paint } = List.foldl - (\\attr ({ top, layout, paint } as lists) -> + (\\attr lists -> case attr of Top key val -> - { lists | top = ( key, val ) :: top } + { lists | top = ( key, val ) :: lists.top } Paint key val -> - { lists | paint = ( key, val ) :: paint } + { lists | paint = ( key, val ) :: lists.paint } Layout key val -> - { lists | layout = ( key, val ) :: layout } + { lists | layout = ( key, val ) :: lists.layout } ) { top = [], layout = [], paint = [] } attrs @@ -155,7 +162,7 @@ encodeAttrs attrs = {-| The background color or pattern of the map. -} background : String -> List (LayerAttr Background) -> Layer -background tipe id attrs = +background id attrs = [ ( "type", Encode.string "background" ) , ( "id", Encode.string id ) ] @@ -244,116 +251,210 @@ visible : Expression CameraExpression Bool -> LayerAttr any visible vis = Layout "visibility" <| Expression.encode <| Expression.ifElse vis (Expression.str "visible") (Expression.str "none") -${Object.entries(codes).map(([section, codes]) => `-- ${section}\n\n${codes.join('\n')}`).join('\n\n')} -` +${Object.entries(codes) + .map(([section, codes]) => `-- ${section}\n\n${codes.join("\n")}`) + .join("\n\n")} +`; +} + +function codeSnippet(name, type) { + return "`" + camelCase(name, type) + "`"; } function requires(req) { - if (typeof req === 'string') { - return `Requires \`${camelCase(req)}\`.`; - } else if (req['!']) { - return `Disabled by \`${camelCase(req['!'])}\`.`; - } else if (req['<=']) { - return `Must be less than or equal to \`${camelCase(req['<='])}\`.`; + if (typeof req === "string") { + return `Requires ${codeSnippet(req)}.`; + } else if (req["!"]) { + return `Disabled by ${codeSnippet(req["!"])}.`; + } else if (req["<="]) { + return `Must be less than or equal to \`${camelCase(req["<="])}\`.`; } else { - const [name, value] = Object.entries(req)[0]; - if (Array.isArray(value)) { - return `Requires \`${camelCase(name)}\` to be ${ - value - .reduce((prev, curr) => [prev, ', or ', curr])}.`; - } else { - return `Requires \`${camelCase(name)}\` to be \`${value}\`.`; - } + const [name, value] = Object.entries(req)[0]; + if (Array.isArray(value)) { + return `Requires ${codeSnippet(name)} to be ${value.slice(1).reduce( + (prev, curr) => prev + ", or " + codeSnippet(curr), codeSnippet(value[0]) + )}.`; + } else { + return `Requires ${codeSnippet(name)} to be \`${value}\`.`; + } } } function generateElmProperty(name, prop, layerType, position) { - if (name == 'visibility') return '' - if (prop['property-type'] === 'constant') throw "Constant property type not supported"; + if (name == "visibility") return ""; + if (prop["property-type"] === "constant") + throw "Constant property type not supported"; const elmName = camelCase(name); - const exprKind = prop['sdk-support']['data-driven styling'] && prop['sdk-support']['data-driven styling'].js ? 'any' : 'CameraExpression'; + const exprKind = + prop["sdk-support"]["data-driven styling"] && + prop["sdk-support"]["data-driven styling"].js + ? "any" + : "CameraExpression"; const exprType = getElmType(prop); - let bounds = ''; - if ('minimum' in prop && 'maximum' in prop) { - bounds = `\n\nShould be between \`${prop.minimum}\` and \`${prop.maximum}\` inclusive. ` - } else if ('minimum' in prop) { - bounds = `\n\nShould be greater than or equal to \`${prop.minimum}\`. ` - } else if ('maximum' in prop) { - bounds = `\n\nShould be less than or equal to \`${prop.maximum}\`. ` + let bounds = ""; + if ("minimum" in prop && "maximum" in prop) { + bounds = `\n\nShould be between \`${prop.minimum}\` and \`${ + prop.maximum + }\` inclusive. `; + } else if ("minimum" in prop) { + bounds = `\n\nShould be greater than or equal to \`${prop.minimum}\`. `; + } else if ("maximum" in prop) { + bounds = `\n\nShould be less than or equal to \`${prop.maximum}\`. `; + } + let valueHelp = ""; + if (exprType == "(Anchor Never)" || exprType == "(Anchor Auto)") { + valueHelp = Object.entries(prop.values) + .map( + ([value, { doc }]) => + `\n- ${codeSnippet(value, exprType)}: ${docify(doc)}` + ) + .join(""); } return ` -{-| ${prop.doc.replace(/`(\w+\-.+?)`/g, str => '`' + camelCase(str.substr(1)))} ${position} property. ${bounds}${prop.units ? `\nUnits in ${prop.units}. ` : ''}${prop.default !== undefined ? 'Defaults to `' + prop.default + '`. ' : ''}${prop.requires ? prop.requires.map(requires).join(' ') : ''} +{-| ${docify(prop.doc, elmName)} ${position} property. ${bounds}${ + prop.units ? `\nUnits in ${prop.units}. ` : "" + }${prop.default !== undefined && elmName !== 'heatmapColor' && elmName !== 'textFont' ? "Defaults to `" + camelCase(prop.default.toString(), exprType) + "`. " : ""}${ + prop.requires ? prop.requires.map(requires).join(" ") : "" + }${valueHelp} -} ${elmName} : Expression ${exprKind} ${exprType} -> LayerAttr ${layerType} ${elmName} = - Expression.encode >> ${position} "${name}"` + Expression.encode >> ${position} "${name}"`; +} + +const enumMap = { + "map | viewport | auto": "(Anchor Auto)", + "map | viewport": "(Anchor Never)", + "left | center | right": "TextJustify", + "center | left | right | top | bottom | top-left | top-right | bottom-left | bottom-right": + "Position", + "none | width | height | both": "TextFit", + "butt | round | square": "LineCap", + "bevel | round | miter": "LineJoin", + "point | line | line-center": "SymbolPlacement", + "none | uppercase | lowercase": "TextTransform", + "linear | nearest": "RasterResampling" +}; + +const flatEnumMap = Object.assign(Object.entries(enumMap).reduce((res, [values, tipe]) => values.split(' | ').reduce((obj, value) => Object.assign({}, obj, {[value]: tipe}), res), {}), { + map: 'Anchor', + viewport: 'Anchor', + auto: "Anchor" +}); + +const reverseEnumMap = Object.entries(enumMap).reduce((res, [values, tipe]) => tipe ==="(Anchor Auto)" || tipe === "(Anchor Never)" ? res : Object.assign({}, res, {[tipe]: values.split(' | ')}), {}) + +console.error(reverseEnumMap) + +function docify(str, name) { + switch (name) { + case "lineGradient": + case "lineDasharray": + return str; + case "heatmapColor": + return `Defines the color of each pixel based on its density value in a heatmap. The value should be an Expression that uses \`heatmapDensity\` as input. Defaults to: + + E.heatmapDensity + |> E.interpolate E.Linear + [ (0.0, rgba 0 0 255 0) + , (0.1, rgba 65 105 225 1) + , (0.3, rgba 0 255 255 1) + , (0.5, rgba 0 255 0 1) + , (0.7, rgba 255 255 0 1) + , (1.0, rgba 255 0 0 1)]` + case "textField": + return `Value to use for a text label.`; + } + ///`(\w+\-.+?)`/g + return str.replace( + /`(.+?)`/g, + (str, match) => "`" + camelCase(match) + "`" + ) } -function getElmType({type, value, values}) { - switch(type) { - case 'number': - return 'Float'; - case 'boolean': +function getElmType({ type, value, values }) { + switch (type) { + case "number": + return "Float"; + case "boolean": return "Bool"; - case 'string': - return 'String'; - case 'color': - return 'Color'; - case 'array': - switch(value) { - case 'number': - return '(Array Float)'; - case 'string': - return '(Array String)'; - } - case 'enum': - switch(Object.keys(values).join(' | ')) { - case "map | viewport": - return 'Anchor'; - case "map | viewport | auto": - return 'AnchorAuto'; - case "center | left | right | top | bottom | top-left | top-right | bottom-left | bottom-right": - return 'Position'; - case 'none | width | height | both': - return 'TextFit'; - case 'butt | round | square': - return 'LineCap'; - case 'bevel | round | miter': - return 'LineJoin'; - case 'point | line': - return 'SymbolPlacement'; - case 'left | center | right': - return 'TextJustify'; - case 'none | uppercase | lowercase': - return 'TextTransform'; + case "string": + return "String"; + case "color": + return "Color"; + case "array": + switch (value) { + case "number": + return "(Array Float)"; + case "string": + return "(Array String)"; } + case "formatted": + return "FormattedText"; + case "enum": + const res = enumMap[Object.keys(values).join(" | ")]; + if (res) return res; } - throw `Unknown type ${type}` + throw `Unknown type ${type}, ${value}, ${values && Object.keys(values)}`; } function titleCase(str) { - return str.replace(/\-/, ' ').replace( - /\w\S*/g, - function(txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - } - ).replace(/\s/, ''); - } - -function camelCase(str) { - return str.replace(/(?:^\w|[A-Z]|\b\w|\-\w)/g, function(letter, index) { - return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); - }).replace(/(?:\s|\-)+/g, ''); + return str + .replace(/\-/, " ") + .replace(/\w\S*/g, function(txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }) + .replace(/\s/, ""); } +function camelCase(str, type) { + if (type && reverseEnumMap[type]) { + str = type + ' ' + str; + } else if (flatEnumMap[str]) { + str = flatEnumMap[str] + ' ' + str; + } else if (str === "rgba(0, 0, 0, 0)") { + return "rgba 0 0 0 0" + } + return str + .replace(/(?:^\w|[A-Z]|\b\w|\-\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }) + .replace(/(?:\s|\-)+/g, ""); +} function makeSignatures(name, constants) { return `{-| -} type ${name} = ${name} - ${constants.split(' | ').map(c => ` + ${constants + .split(" | ") + .map( + c => ` {-| -} -${camelCase(name + ' ' + c)} : Expression exprType ${name} -${camelCase(name + ' ' + c)} = Expression (Json.Encode.string "${c}") -`).join('\n')}` +${camelCase(name + " " + c)} : Expression exprType ${name} +${camelCase(name + " " + c)} = Expression (Json.Encode.string "${c}") +` + ) + .join("\n")}`; } + +/// --- NODEJS STUFF + +var stdin = process.stdin, + stdout = process.stdout, + inputChunks = []; + +stdin.resume(); +stdin.setEncoding("utf8"); + +stdin.on("data", function(chunk) { + inputChunks.push(chunk); +}); + +stdin.on("end", function() { + var inputJSON = inputChunks.join(""), + parsedData = JSON.parse(inputJSON), + output = generateProperties(parsedData); + stdout.write(output); + stdout.write("\n"); +}); |