Skip to content

Commit 91ec647

Browse files
authored
Merge pull request #384 from evolvedbinary/feature/validate-and-error
Validate the attributes and throw errors
2 parents c746700 + b380bdb commit 91ec647

File tree

4 files changed

+99
-18
lines changed

4 files changed

+99
-18
lines changed

packages/lwdita-ast/src/nodes/base.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,14 @@ export abstract class AbstractBaseNode implements BaseNode {
164164
* @param attributes - Attributes of the node
165165
* @returns An object with the record of properties
166166
*/
167-
static attributesToProps<T extends Record<string, BasicValue>>(attributes: Attributes = {}): T {
167+
static attributesToProps<T extends Record<string, BasicValue>>(
168+
attributes: Attributes = {},
169+
): T {
170+
Object.keys(attributes).forEach((attrName) => {
171+
if (!this.fields.includes(attrName)) {
172+
throw new UnknownAttributeError(`Unknown attribute: "${attrName}"`);
173+
}
174+
});
168175
const result: Record<string, BasicValue> = {};
169176
// loop through all node attributes and get their values
170177
this.fields.forEach(field => {
@@ -276,7 +283,11 @@ export abstract class AbstractBaseNode implements BaseNode {
276283
// If there is a child that cannot be added, throw a new error
277284
if (!this.canAddNode(child)) {
278285
if (breakOnError) {
279-
throw new NonAcceptedChildError(`"${child.static.nodeName}" node can't be a child of "${this.static.nodeName}" node`);
286+
if(child.static.nodeName === "text") {
287+
throw new NonAcceptedChildError(`${child.static.nodeName} node can't be a child of "${this.static.nodeName}" node`);
288+
} else {
289+
throw new NonAcceptedChildError(`"${child.static.nodeName}" node can't be a child of "${this.static.nodeName}" node`);
290+
}
280291
}
281292
return;
282293
}

packages/lwdita-ast/test/base.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,21 @@ describe('Base Node children (groups)', () => {
541541
});
542542
});
543543

544+
describe('Node attributes', () => {
545+
class SampleNode extends AbstractBaseNode {
546+
static nodeName = 'child';
547+
static fields: string[] = ["foo", "bar", "baz"];
548+
}
549+
it("Throws when a wrong attribute is set", () => {
550+
expect(() => {
551+
new SampleNode({
552+
"foo": "bar",
553+
"wrong-attr": "wrong"
554+
})
555+
}).to.throw('Unknown attribute: "wrong-attr"');
556+
})
557+
});
558+
544559
describe('JDita', () => {
545560
it('Empty node', () => {
546561
class Node extends AbstractBaseNode {

packages/lwdita-xdita/src/converter.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717

1818
import * as saxes from "@rubensworks/saxes";
1919
import { createCDataSectionNode, createNode } from "./factory";
20-
import { Attributes, BasicValue, TextNode, getNodeClass, JDita, BaseNode, DocumentNode, DocTypeDecl, CDataNode, AbstractBaseNode, XMLDecl } from "@evolvedbinary/lwdita-ast";
20+
import { Attributes, BasicValue, TextNode, getNodeClass, JDita, BaseNode, DocumentNode, DocTypeDecl, CDataNode, AbstractBaseNode, XMLDecl, NonAcceptedChildError, UnknownAttributeError } from "@evolvedbinary/lwdita-ast";
2121
import { InMemoryTextSimpleOutputStreamCollector } from "./stream";
2222
import { XditaSerializer } from "./xdita-serializer";
23+
import { formatErrorMessage } from "./utils";
2324

2425
/**
2526
* Converts XML to an AST document tree
2627
*
2728
* @param xml - XML string
28-
* @param abortOnError - If true, abort on error
29+
* @param abortOnFirstError - If true, abort on first error
2930
* @returns - Promise of a DocumentNode
3031
*/
31-
export async function xditaToAst(xml: string, abortOnError = true): Promise<DocumentNode> {
32+
export async function xditaToAst(xml: string, abortOnFirstError = true): Promise<DocumentNode> {
3233
return new Promise((resolve, reject) => {
3334
const errors: Error[] = [];
3435
// Create a Parser Object
@@ -79,7 +80,15 @@ export async function xditaToAst(xml: string, abortOnError = true): Promise<Docu
7980

8081
const node: BaseNode = createNode(text);
8182
// add the text node to the parent
82-
stack[stack.length - 1].add(node, abortOnError);
83+
try {
84+
stack[stack.length - 1].add(node, abortOnFirstError);
85+
} catch (e) {
86+
if(e instanceof NonAcceptedChildError) {
87+
errorHandler(e);
88+
} else {
89+
throw e
90+
}
91+
}
8392
});
8493

8594
// Look for the first open tag `<` and add the node to the array
@@ -110,11 +119,15 @@ export async function xditaToAst(xml: string, abortOnError = true): Promise<Docu
110119
*/
111120
parser.on("opentag", function (node: saxes.SaxesTagNS) {
112121
try {
113-
const obj = createNode(node);
114-
stack[stack.length - 1].add(obj, abortOnError);
115-
stack.push(obj);
122+
const obj = createNode(node);
123+
stack[stack.length - 1].add(obj, abortOnFirstError);
124+
stack.push(obj);
116125
} catch (e) {
117-
console.log('invalid:', e);
126+
if(e instanceof NonAcceptedChildError || e instanceof UnknownAttributeError) {
127+
errorHandler(e, node.name);
128+
} else {
129+
throw e
130+
}
118131
}
119132
});
120133

@@ -127,24 +140,37 @@ export async function xditaToAst(xml: string, abortOnError = true): Promise<Docu
127140
parser.on("cdata", (cdata) => {
128141
try {
129142
const obj = createCDataSectionNode(cdata);
130-
stack[stack.length - 1].add(obj, abortOnError);
143+
stack[stack.length - 1].add(obj, abortOnFirstError);
131144
} catch (e) {
132-
console.log('invalid:', e);
145+
if(e instanceof NonAcceptedChildError) {
146+
errorHandler(e, "CDATA");
147+
} else {
148+
throw e
149+
}
133150
}
134151
})
135152

136153
// return the document tree if run without errors
137154
parser.on("end", function () {
138-
if (errors.length && abortOnError) {
155+
if (errors.length) {
139156
reject(errors);
140157
} else {
141158
resolve(doc);
142159
}
143160
});
144161

145-
parser.on("error", function (e) {
162+
function errorHandler(e: Error, nodeName?: string) {
163+
e.message = formatErrorMessage(e.message, stack, parser.line, parser.column,nodeName)
164+
if(abortOnFirstError) {
165+
reject(e.message)
166+
// This is necessary to stop the parsing
167+
throw e;
168+
}
146169
errors.push(e);
147-
});
170+
}
171+
172+
// Error handler for XML
173+
parser.on("error", errorHandler);
148174

149175
// process the xml using the parser
150176
parser.write(xml).close();
@@ -155,11 +181,11 @@ export async function xditaToAst(xml: string, abortOnError = true): Promise<Docu
155181
* Convert the document tree to JDita object
156182
*
157183
* @param xml - The XML input as string
158-
* @param abortOnError - Boolean, if true, stop execution and report errors
184+
* @param abortOnFirstError - Boolean, if true, stop execution and report errors
159185
* @returns JDita object
160186
*/
161-
export async function xditaToJdita(xml: string, abortOnError = true): Promise<JDita> {
162-
return xditaToAst(xml, abortOnError).then(astToJdita);
187+
export async function xditaToJdita(xml: string, abortOnFirstError = true): Promise<JDita> {
188+
return xditaToAst(xml, abortOnFirstError).then(astToJdita);
163189
}
164190

165191
/**

packages/lwdita-xdita/src/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License
1515
along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
*/
1717

18+
import { BaseNode } from '@evolvedbinary/lwdita-ast';
1819
import fs from 'fs';
1920

2021
/**
@@ -64,4 +65,32 @@ export function escapeXMLAttributeCharacters(text: string): string {
6465
default: return match;
6566
}
6667
})
68+
}
69+
/**
70+
* Get the current node path from the stack
71+
* e.g. "topic/section/paragraph/text"
72+
*/
73+
export function getPathFromStack(stack: BaseNode[]): string {
74+
// Don't mutate the original stack
75+
const path = stack.map(node => node.static.nodeName);
76+
// remove the document node
77+
path.shift();
78+
return path.join("/");
79+
}
80+
81+
82+
export function formatErrorMessage(msg: string, stack: BaseNode[], line: number, column: number, nodeName?: string) {
83+
let error = msg
84+
if(nodeName) {
85+
error += ` in node ${nodeName}`
86+
}
87+
error += `\n`;
88+
if(nodeName) {
89+
error += `Path: /${getPathFromStack(stack)}/${nodeName}\n`
90+
} else {
91+
error += `Path: /${getPathFromStack(stack)}\n`
92+
}
93+
error += `Parsing Error at line ${line}, column ${column} \n`
94+
95+
return error;
6796
}

0 commit comments

Comments
 (0)