diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.js | 218 |
1 files changed, 147 insertions, 71 deletions
diff --git a/src/index.js b/src/index.js index f705f5a..729db76 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,13 @@ /** * A parsed wardley map. * @typedef {Object} Map - * @property {Entity[]} entities - The list of parsed entities - */ - -/** - * Any of the potential entities in the map. - * @typedef {Component|Dependency|Note|Stage|Group|Inertia|Evolution} Entity + * @property {Component[]} components - List of components + * @property {Dependency[]} dependencies - List of dependencies + * @property {Note[]} notes - List of notes + * @property {Stage[]} stages - List of stages + * @property {Group[]} groups - List of groups + * @property {Inertia[]} inertias - List of inertias + * @property {Evolution[]} evolutions - List of evolutions */ /** @@ -17,7 +18,6 @@ /** * A component in the map. * @typedef {Object} Component - * @property {'component'} type - Entity type * @property {string} label - Component label * @property {[number, number]} coordinates - X and Y coordinates * @property {Shape} shape - Shape of the component @@ -26,7 +26,6 @@ /** * A dependency between two components. * @typedef {Object} Dependency - * @property {'dependency'} type - Entity type * @property {string} from - Source component label * @property {string} to - Target component label * @property {boolean} isDirected - Whether the dependency is directed (->) or undirected (--) @@ -35,7 +34,6 @@ /** * A note. * @typedef {Object} Note - * @property {'note'} type - Entity type * @property {[number, number]} coordinates - X and Y coordinates * @property {string} text - Note text content */ @@ -43,7 +41,6 @@ /** * An override for the width of an evolution stage. * @typedef {Object} Stage - * @property {'stage'} type - Entity type * @property {string} stage - Stage number (i, ii, iii, iv) * @property {number} value - Stage value */ @@ -51,63 +48,112 @@ /** * A group of components. * @typedef {Object} Group - * @property {'group'} type - Entity type * @property {string[]} components - Array of component labels in the group */ /** * Inertia associated with a component. * @typedef {Object} Inertia - * @property {'inertia'} type - Entity type * @property {string} component - Component label with inertia */ /** * Evolution associated with a component. * @typedef {Object} Evolution - * @property {'evolution'} type - Entity type * @property {string} component - Component label * @property {number} value - Evolution value */ -/** - * Helper: Check if character is whitespace - */ -function isWhitespace(character) { - return character === " " || character === "\t"; -} +/* CONSTANTS ******************************************************************/ -/** - * Helper: Check if character is a digit or dot - */ -function isDigitOrDot(character) { - return (character >= "0" && character <= "9") || character === "."; -} +const kLineSplitter = /\r\n|\r|\n/; + +/* PUBLIC API *****************************************************************/ /** * wmap format Wardley Map parser. * @param {string} source - The wmap source code to parse - * @returns {Map} - Parsed map with entities + * @returns {Map} - Parsed map with separate arrays for each entity type */ export function parse(source) { - const entities = []; - const lines = source.split(/\r\n|\r|\n/); + const components = []; + const dependencies = []; + const notes = []; + const stages = []; + const groups = []; + const inertias = []; + const evolutions = []; + const lines = source.split(kLineSplitter); for (const rawLine of lines) { const line = rawLine.trim(); if (!line) continue; const entity = parseLine(line); - if (entity) { - entities.push(entity); + if (!entity) continue; + + switch (entity.type) { + case "component": + components.push({ + label: entity.label, + coordinates: entity.coordinates, + shape: entity.shape, + }); + break; + case "dependency": + dependencies.push({ + from: entity.from, + to: entity.to, + isDirected: entity.isDirected, + }); + break; + case "note": + notes.push({ + coordinates: entity.coordinates, + text: entity.text, + }); + break; + case "stage": + stages.push({ + stage: entity.stage, + value: entity.value, + }); + break; + case "group": + groups.push({ + components: entity.components, + }); + break; + case "inertia": + inertias.push({ + component: entity.component, + }); + break; + case "evolution": + evolutions.push({ + component: entity.component, + value: entity.value, + }); + break; } } - return { entities }; + return { + components, + dependencies, + notes, + stages, + groups, + inertias, + evolutions, + }; } +/* PARSERS ********************************************************************/ + /** - * Parse a line + * Routes a line to either the keyword parser, or the component / dependency + * parser. */ function parseLine(line) { const length = line.length; @@ -125,41 +171,24 @@ function parseLine(line) { } /** - * Parse entities that start with keywords in brackets: [Stage], [Note], [Group], [Inertia], [Evolution] + * Parse any entity that start with keywords in brackets: [Stage], [Note], + * [Group], [Inertia], [Evolution]. */ function parseKeywordEntity(line, start, length) { - let i = start + 1; // skip '[' + // Read after [... + let i = start + 1; const keywordStart = i; - // Read until ']' + // ...but before ] while (i < length && line[i] !== "]") i++; if (i >= length) return null; const keyword = line.substring(keywordStart, i).trim().toLowerCase(); - i++; // skip ']' - // Skip whitespace after ']' + // Skip ] and whitespace + i++; while (i < length && isWhitespace(line[i])) i++; - // Stage: [I|II|III|IV] stage - if ( - keyword === "i" || - keyword === "ii" || - keyword === "iii" || - keyword === "iv" - ) { - const numStart = i; - while (i < length && isDigitOrDot(line[i])) i++; - if (i > numStart) { - return { - type: "stage", - stage: keyword, - value: parseFloat(line.substring(numStart, i)), - }; - } - return null; - } - // Note: [Note] (x, y) text if (keyword === "note") { // Expect '(' @@ -206,8 +235,8 @@ function parseKeywordEntity(line, start, length) { const components = line .substring(i) .split(",") - .map(v => v.trim()) - .filter(v => v); + .map((v) => v.trim()) + .filter((v) => v); return components.length > 0 ? { type: "group", components } : null; } @@ -226,7 +255,10 @@ function parseKeywordEntity(line, start, length) { for (let j = i; j < length; j++) { if (line[j] === "+" || line[j] === "-") { // Make sure it's not part of a dependency arrow - if (j + 1 < length && (line[j + 1] === ">" || line[j + 1] === "-")) + if ( + j + 1 < length && + (line[j + 1] === ">" || line[j + 1] === "-") + ) continue; indexOfSign = j; signCharacter = line[j]; @@ -241,18 +273,41 @@ function parseKeywordEntity(line, start, length) { // Read number after sign let indexOfNumber = indexOfSign + 1; - while (indexOfNumber < length && isWhitespace(line[indexOfNumber])) indexOfNumber++; - const numStart = indexOfNumber; - while (indexOfNumber < length && isDigitOrDot(line[indexOfNumber])) indexOfNumber++; - if (indexOfNumber === numStart) return null; + while (indexOfNumber < length && isWhitespace(line[indexOfNumber])) + indexOfNumber++; + const startOfNumber = indexOfNumber; + while (indexOfNumber < length && isDigitOrDot(line[indexOfNumber])) + indexOfNumber++; + if (indexOfNumber === startOfNumber) return null; return { type: "evolution", component, - value: (signCharacter === "+" ? 1 : -1) * parseFloat(line.substring(numStart, indexOfNumber)), + value: + (signCharacter === "+" ? 1 : -1) * + parseFloat(line.substring(startOfNumber, indexOfNumber)), }; } + // Stage: [I|II|III|IV] stage + // Least common. Max 4 in a file. + if ( + keyword === "i" || + keyword === "ii" || + keyword === "iii" || + keyword === "iv" + ) { + const startOfNumber = i; + while (i < length && isDigitOrDot(line[i])) i++; + if (i > startOfNumber) { + return { + type: "stage", + stage: keyword, + value: parseFloat(line.substring(startOfNumber, i)), + }; + } + } + return null; } @@ -260,13 +315,15 @@ function parseKeywordEntity(line, start, length) { * Parse component or dependency */ function parseComponentOrDependency(line, length) { - // Use indexOf to quickly find potential dependency arrows and parentheses const indexOfArrow = line.indexOf("->"); const indexOfDash = line.indexOf("--"); const indexOfParenthesis = line.indexOf("("); - // Check for directed dependency (->) - if (indexOfArrow !== -1 && (indexOfParenthesis === -1 || indexOfArrow < indexOfParenthesis)) { + // Directed dependency + if ( + indexOfArrow !== -1 && + (indexOfParenthesis === -1 || indexOfArrow < indexOfParenthesis) + ) { const from = line.substring(0, indexOfArrow).trim(); const to = line.substring(indexOfArrow + 2).trim(); if (from && to) { @@ -280,8 +337,11 @@ function parseComponentOrDependency(line, length) { return null; } - // Check for undirected dependency (--) - if (indexOfDash !== -1 && (indexOfParenthesis === -1 || indexOfDash < indexOfParenthesis)) { + // Undirected dependency + if ( + indexOfDash !== -1 && + (indexOfParenthesis === -1 || indexOfDash < indexOfParenthesis) + ) { const from = line.substring(0, indexOfDash).trim(); const to = line.substring(indexOfDash + 2).trim(); if (from && to) { @@ -295,7 +355,7 @@ function parseComponentOrDependency(line, length) { return null; } - // Check for component (opening parenthesis) + // Component if (indexOfParenthesis > 0) { return parseComponent(line, indexOfParenthesis, length); } @@ -304,7 +364,7 @@ function parseComponentOrDependency(line, length) { } /** - * Parse component starting from the opening parenthesis + * Parse a component. */ function parseComponent(line, indexOfParenthesis, length) { const label = line.substring(0, indexOfParenthesis).trim(); @@ -355,3 +415,19 @@ function parseComponent(line, indexOfParenthesis, length) { shape, }; } + +/* HELPER FUNCTIONS ***********************************************************/ + +/** + * Helper: Check if character is whitespace + */ +function isWhitespace(character) { + return character === " " || character === "\t"; +} + +/** + * Helper: Check if character is a digit or dot + */ +function isDigitOrDot(character) { + return (character >= "0" && character <= "9") || character === "."; +} |