/
translation_utils.ts
175 lines (158 loc) · 6.18 KB
/
translation_utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Element, LexerRange, Node, ParseError, ParseErrorLevel, ParseSourceSpan, ParseTreeResult, XmlParser} from '@angular/compiler';
import {ɵParsedTranslation} from '@angular/localize';
import {Diagnostics} from '../../../diagnostics';
import {MessageSerializer} from '../message_serialization/message_serializer';
import {TranslationParseError} from './translation_parse_error';
import {ParseAnalysis} from './translation_parser';
export function getAttrOrThrow(element: Element, attrName: string): string {
const attrValue = getAttribute(element, attrName);
if (attrValue === undefined) {
throw new TranslationParseError(
element.sourceSpan, `Missing required "${attrName}" attribute:`);
}
return attrValue;
}
export function getAttribute(element: Element, attrName: string): string|undefined {
const attr = element.attrs.find(a => a.name === attrName);
return attr !== undefined ? attr.value : undefined;
}
/**
* Parse the "contents" of an XML element.
*
* This would be equivalent to parsing the `innerHTML` string of an HTML document.
*
* @param element The element whose inner range we want to parse.
* @returns a collection of XML `Node` objects and any errors that were parsed from the element's
* contents.
*/
export function parseInnerRange(element: Element): ParseTreeResult {
const xmlParser = new XmlParser();
const xml = xmlParser.parse(
element.sourceSpan.start.file.content, element.sourceSpan.start.file.url,
{tokenizeExpansionForms: true, range: getInnerRange(element)});
return xml;
}
/**
* Compute a `LexerRange` that contains all the children of the given `element`.
* @param element The element whose inner range we want to compute.
*/
function getInnerRange(element: Element): LexerRange {
const start = element.startSourceSpan!.end;
const end = element.endSourceSpan!.start;
return {
startPos: start.offset,
startLine: start.line,
startCol: start.col,
endPos: end.offset,
};
}
/**
* This "hint" object is used to pass information from `canParse()` to `parse()` for
* `TranslationParser`s that expect XML contents.
*
* This saves the `parse()` method from having to re-parse the XML.
*/
export interface XmlTranslationParserHint {
element: Element;
errors: ParseError[];
}
/**
* Can this XML be parsed for translations, given the expected `rootNodeName` and expected root node
* `attributes` that should appear in the file.
*
* @param filePath The path to the file being checked.
* @param contents The contents of the file being checked.
* @param rootNodeName The expected name of an XML root node that should exist.
* @param attributes The attributes (and their values) that should appear on the root node.
* @returns The `XmlTranslationParserHint` object for use by `TranslationParser.parse()` if the XML
* document has the expected format.
*/
export function canParseXml(
filePath: string, contents: string, rootNodeName: string,
attributes: Record<string, string>): ParseAnalysis<XmlTranslationParserHint> {
const diagnostics = new Diagnostics();
const xmlParser = new XmlParser();
const xml = xmlParser.parse(contents, filePath);
if (xml.rootNodes.length === 0 ||
xml.errors.some(error => error.level === ParseErrorLevel.ERROR)) {
xml.errors.forEach(e => addParseError(diagnostics, e));
return {canParse: false, diagnostics};
}
const rootElements = xml.rootNodes.filter(isNamedElement(rootNodeName));
const rootElement = rootElements[0];
if (rootElement === undefined) {
diagnostics.warn(`The XML file does not contain a <${rootNodeName}> root node.`);
return {canParse: false, diagnostics};
}
for (const attrKey of Object.keys(attributes)) {
const attr = rootElement.attrs.find(attr => attr.name === attrKey);
if (attr === undefined || attr.value !== attributes[attrKey]) {
addParseDiagnostic(
diagnostics, rootElement.sourceSpan,
`The <${rootNodeName}> node does not have the required attribute: ${attrKey}="${
attributes[attrKey]}".`,
ParseErrorLevel.WARNING);
return {canParse: false, diagnostics};
}
}
if (rootElements.length > 1) {
xml.errors.push(new ParseError(
xml.rootNodes[1].sourceSpan,
'Unexpected root node. XLIFF 1.2 files should only have a single <xliff> root node.',
ParseErrorLevel.WARNING));
}
return {canParse: true, diagnostics, hint: {element: rootElement, errors: xml.errors}};
}
/**
* Create a predicate, which can be used by things like `Array.filter()`, that will match a named
* XML Element from a collection of XML Nodes.
*
* @param name The expected name of the element to match.
*/
export function isNamedElement(name: string): (node: Node) => node is Element {
function predicate(node: Node): node is Element {
return node instanceof Element && node.name === name;
}
return predicate;
}
/**
* Add an XML parser related message to the given `diagnostics` object.
*/
export function addParseDiagnostic(
diagnostics: Diagnostics, sourceSpan: ParseSourceSpan, message: string,
level: ParseErrorLevel): void {
addParseError(diagnostics, new ParseError(sourceSpan, message, level));
}
/**
* Copy the formatted error message from the given `parseError` object into the given `diagnostics`
* object.
*/
export function addParseError(diagnostics: Diagnostics, parseError: ParseError): void {
if (parseError.level === ParseErrorLevel.ERROR) {
diagnostics.error(parseError.toString());
} else {
diagnostics.warn(parseError.toString());
}
}
/**
* Serialize the given `element` into a parsed translation using the given `serializer`.
*/
export function serializeTargetMessage(
element: Element, serializer: MessageSerializer<ɵParsedTranslation>):
{translation: ɵParsedTranslation|null, errors: ParseError[]} {
const {rootNodes, errors} = parseInnerRange(element);
let translation: ɵParsedTranslation|null = null;
try {
translation = serializer.serialize(rootNodes);
} catch (e) {
errors.push(e);
}
return {translation, errors};
}