aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/index.js218
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 === ".";
+}