Add support for TOML symbol metadata. (#33)
This fixes #20 by adding support for TOML-based metadata for symbols. There's now a template TOML file in `svg/_template.toml` that documents all the different metadata properties. The file can be copied to match the name of an SVG file (but with a `.toml` extension) and it will be processed by the vocabulary builder when the site is generated. Currently there are only two properties documented, `always_nest` and `always_be_nested` (as per the needs outlined in https://github.com/toolness/mystic-symbolic/issues/17#issuecomment-786696966), but they don't actually do anything yet (actual support for _using_ the metadata will come in another file). Right now the TOML files are validated quite stringently: if a file contains the name of a property it doesn't understand, or the type of the property is wrong, it will raise an error. We can revisit this if it becomes burdensome.pull/34/head
rodzic
8e46e4d5f1
commit
3e1b66a984
|
@ -0,0 +1,46 @@
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import toml from "toml";
|
||||||
|
import { validateSvgSymbolMetadata } from "./svg-symbol-metadata";
|
||||||
|
|
||||||
|
const templatePath = path.join(__dirname, "..", "svg", "_template.toml");
|
||||||
|
|
||||||
|
test("metadata template is valid SVG symbol metadata", () => {
|
||||||
|
validateSvgSymbolMetadata(
|
||||||
|
toml.parse(
|
||||||
|
fs.readFileSync(templatePath, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateSvgSymbolMetadata()", () => {
|
||||||
|
it("works with valid metadata", () => {
|
||||||
|
expect(
|
||||||
|
validateSvgSymbolMetadata({
|
||||||
|
always_nest: true,
|
||||||
|
always_be_nested: true,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
always_nest: true,
|
||||||
|
always_be_nested: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("raises errors when a property is of the wrong type", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSvgSymbolMetadata({
|
||||||
|
always_nest: "true",
|
||||||
|
})
|
||||||
|
).toThrow('Expected "always_nest" to be a boolean, but it is a string!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("raises errors when a property is unrecognized", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSvgSymbolMetadata({
|
||||||
|
blarp: true,
|
||||||
|
})
|
||||||
|
).toThrow('Unrecognized SVG symbol metadata property "blarp"');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
type SvgSymbolMetadataBooleans = {
|
||||||
|
/**
|
||||||
|
* If true, this indicates that the symbol should always have
|
||||||
|
* a symbol nested within its nesting area(s).
|
||||||
|
*/
|
||||||
|
always_nest?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, this indicates that the symbol should always
|
||||||
|
* be nested inside another symbol's nesting area.
|
||||||
|
*/
|
||||||
|
always_be_nested?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const METADATA_BOOLEANS: Set<keyof SvgSymbolMetadataBooleans> = new Set([
|
||||||
|
"always_nest",
|
||||||
|
"always_be_nested",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isSvgSymbolMetadataBoolean(
|
||||||
|
key: string
|
||||||
|
): key is keyof SvgSymbolMetadataBooleans {
|
||||||
|
return METADATA_BOOLEANS.has(key as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SvgSymbolMetadata = SvgSymbolMetadataBooleans;
|
||||||
|
|
||||||
|
export function validateSvgSymbolMetadata(obj: any): SvgSymbolMetadata {
|
||||||
|
const result: SvgSymbolMetadata = {};
|
||||||
|
for (let key in obj) {
|
||||||
|
const value: unknown = obj[key];
|
||||||
|
if (isSvgSymbolMetadataBoolean(key)) {
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new Error(
|
||||||
|
`Expected "${key}" to be a boolean, but it is a ${typeof value}!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result[key] = value;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unrecognized SVG symbol metadata property "${key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { SVGProps } from "react";
|
||||||
import { BBox } from "../vendor/bezier-js";
|
import { BBox } from "../vendor/bezier-js";
|
||||||
import { FILL_REPLACEMENT_COLOR, STROKE_REPLACEMENT_COLOR } from "./colors";
|
import { FILL_REPLACEMENT_COLOR, STROKE_REPLACEMENT_COLOR } from "./colors";
|
||||||
import { Specs } from "./specs";
|
import { Specs } from "./specs";
|
||||||
|
import type { SvgSymbolMetadata } from "./svg-symbol-metadata";
|
||||||
import { VisibleSpecs } from "./visible-specs";
|
import { VisibleSpecs } from "./visible-specs";
|
||||||
|
|
||||||
const DEFAULT_UNIFORM_STROKE_WIDTH = 1;
|
const DEFAULT_UNIFORM_STROKE_WIDTH = 1;
|
||||||
|
@ -11,6 +12,7 @@ export type SvgSymbolData = {
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BBox;
|
bbox: BBox;
|
||||||
layers: SvgSymbolElement[];
|
layers: SvgSymbolElement[];
|
||||||
|
meta?: SvgSymbolMetadata;
|
||||||
specs?: Specs;
|
specs?: Specs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import cheerio from "cheerio";
|
||||||
import { getSvgBoundingBox } from "./bounding-box";
|
import { getSvgBoundingBox } from "./bounding-box";
|
||||||
import { extractSpecs } from "./specs";
|
import { extractSpecs } from "./specs";
|
||||||
import { SvgSymbolData, SvgSymbolElement } from "./svg-symbol";
|
import { SvgSymbolData, SvgSymbolElement } from "./svg-symbol";
|
||||||
|
import toml from "toml";
|
||||||
|
import { validateSvgSymbolMetadata } from "./svg-symbol-metadata";
|
||||||
|
|
||||||
const SUPPORTED_SVG_TAG_ARRAY: SvgSymbolElement["tagName"][] = ["g", "path"];
|
const SUPPORTED_SVG_TAG_ARRAY: SvgSymbolElement["tagName"][] = ["g", "path"];
|
||||||
const SUPPORTED_SVG_TAGS = new Set(SUPPORTED_SVG_TAG_ARRAY);
|
const SUPPORTED_SVG_TAGS = new Set(SUPPORTED_SVG_TAG_ARRAY);
|
||||||
|
@ -109,11 +111,24 @@ export function build() {
|
||||||
const vocab: SvgSymbolData[] = [];
|
const vocab: SvgSymbolData[] = [];
|
||||||
for (let filename of filenames) {
|
for (let filename of filenames) {
|
||||||
if (path.extname(filename) === SVG_EXT) {
|
if (path.extname(filename) === SVG_EXT) {
|
||||||
console.log(`Adding ${filename} to vocabulary.`);
|
let filenames = filename;
|
||||||
|
let metaToml: string | null = null;
|
||||||
|
const metaFilename = `${path.basename(filename, SVG_EXT)}.toml`;
|
||||||
|
const metaFilepath = path.join(SVG_DIR, metaFilename);
|
||||||
|
if (fs.existsSync(metaFilepath)) {
|
||||||
|
filenames += ` and ${metaFilename}`;
|
||||||
|
metaToml = fs.readFileSync(metaFilepath, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`Adding ${filenames} to vocabulary.`);
|
||||||
const svgMarkup = fs.readFileSync(path.join(SVG_DIR, filename), {
|
const svgMarkup = fs.readFileSync(path.join(SVG_DIR, filename), {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
const symbol = convertSvgMarkupToSymbolData(filename, svgMarkup);
|
const symbol = convertSvgMarkupToSymbolData(filename, svgMarkup);
|
||||||
|
if (metaToml) {
|
||||||
|
symbol.meta = validateSvgSymbolMetadata(toml.parse(metaToml));
|
||||||
|
}
|
||||||
vocab.push(symbol);
|
vocab.push(symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9682,6 +9682,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||||
},
|
},
|
||||||
|
"toml": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||||
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# This can be used as a template for an SVG symbol's metadata.
|
||||||
|
# Just be sure you rename it to have the same name as the SVG, only
|
||||||
|
# with a a ".toml" extension instead of an ".svg" extension.
|
||||||
|
#
|
||||||
|
# So for example, if you want to define metadata for "antler.svg",
|
||||||
|
# you can make a copy of this file and call it "antler.toml".
|
||||||
|
#
|
||||||
|
# Also note that any lines that begin with a "#" are comments,
|
||||||
|
# so they will be ignored by the code.
|
||||||
|
|
||||||
|
# If true, this indicates that the symbol should always have
|
||||||
|
# a symbol nested within its nesting area(s).
|
||||||
|
always_nest = false
|
||||||
|
|
||||||
|
# If true, this indicates that the symbol should always
|
||||||
|
# be nested inside another symbol's nesting area.
|
||||||
|
always_be_nested = false
|
|
@ -0,0 +1,7 @@
|
||||||
|
# If true, this indicates that the symbol should always have
|
||||||
|
# a symbol nested within its nesting area(s).
|
||||||
|
always_nest = true
|
||||||
|
|
||||||
|
# If true, this indicates that the symbol should always
|
||||||
|
# be nested inside another symbol's nesting area.
|
||||||
|
always_be_nested = false
|
Ładowanie…
Reference in New Issue