327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
|
/**
|
|||
|
* 基于 xsd 文件生成 typescript 定义,简化开发
|
|||
|
*/
|
|||
|
import * as fs from 'fs';
|
|||
|
|
|||
|
import {XMLParser} from 'fast-xml-parser';
|
|||
|
|
|||
|
const parser = new XMLParser({
|
|||
|
ignoreAttributes: false,
|
|||
|
allowBooleanAttributes: true,
|
|||
|
trimValues: false,
|
|||
|
alwaysCreateTextNode: true
|
|||
|
});
|
|||
|
|
|||
|
function parseXML(xml: string) {
|
|||
|
return parser.parse(xml);
|
|||
|
}
|
|||
|
|
|||
|
let outputFile: string[] = ['/** generated by tools/xsd2ts.ts, do not edit */'];
|
|||
|
|
|||
|
// 所有定义过的类型
|
|||
|
const definedTypes: Record<string, boolean> = {};
|
|||
|
|
|||
|
// 所有使用的类型
|
|||
|
const allusedTypes: Record<string, boolean> = {};
|
|||
|
|
|||
|
outputFile.push(
|
|||
|
...convertSchema(
|
|||
|
'./OfficeOpenXML-XMLSchema-Strict/shared-commonSimpleTypes.xsd'
|
|||
|
)
|
|||
|
);
|
|||
|
outputFile.push(...convertSchema('./OfficeOpenXML-XMLSchema-Strict/wml.xsd'));
|
|||
|
outputFile.push(
|
|||
|
...convertSchema('./OfficeOpenXML-XMLSchema-Strict/dml-main.xsd')
|
|||
|
);
|
|||
|
|
|||
|
outputFile.push(
|
|||
|
...convertSchema(
|
|||
|
'./OfficeOpenXML-XMLSchema-Strict/dml-wordprocessingDrawing.xsd'
|
|||
|
)
|
|||
|
);
|
|||
|
|
|||
|
function generateField(attributes: any[]) {
|
|||
|
const result: string[] = [];
|
|||
|
|
|||
|
if (!Array.isArray(attributes)) {
|
|||
|
attributes = [attributes];
|
|||
|
}
|
|||
|
for (const attribute of attributes) {
|
|||
|
const attributeName = attribute['@_name'];
|
|||
|
if (attributeName) {
|
|||
|
let attributeType = attribute['@_type']
|
|||
|
.replace(/s:/g, '')
|
|||
|
.replace(/a:/g, '');
|
|||
|
allusedTypes[attributeType] = true;
|
|||
|
if (attributeType.startsWith('xsd:')) {
|
|||
|
attributeType = toJavaScriptType(attributeType);
|
|||
|
}
|
|||
|
const use = attribute['@_use'];
|
|||
|
if (use === 'required') {
|
|||
|
result.push(` ${attributeName}: ${attributeType};`);
|
|||
|
} else {
|
|||
|
result.push(` ${attributeName}?: ${attributeType};`);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
let ref = attribute['@_ref'];
|
|||
|
if (ref) {
|
|||
|
ref = ref
|
|||
|
.replace(/r:/g, 'r')
|
|||
|
.replace(/m:/g, '')
|
|||
|
.replace(/sl:/g, '')
|
|||
|
.replace(/a:/g, '');
|
|||
|
const use = attribute['@_use'];
|
|||
|
if (ref.indexOf(':') !== -1) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
if (use === 'required') {
|
|||
|
result.push(` ${ref}: string;`);
|
|||
|
} else {
|
|||
|
result.push(` ${ref}?: string;`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
function toJavaScriptType(type: string) {
|
|||
|
if (
|
|||
|
type === 'xsd:string' ||
|
|||
|
type === 'xsd:token' ||
|
|||
|
type === 'xsd:hexBinary' ||
|
|||
|
type === 'xsd:dateTime' ||
|
|||
|
type === 'xsd:NCName' ||
|
|||
|
type === 'xsd:base64Binary'
|
|||
|
) {
|
|||
|
return 'string';
|
|||
|
} else if (
|
|||
|
type === 'xsd:integer' ||
|
|||
|
type === 'xsd:int' ||
|
|||
|
type === 'xsd:unsignedInt' ||
|
|||
|
type === 'xsd:unsignedLong' ||
|
|||
|
type === 'xsd:unsignedShort' ||
|
|||
|
type === 'xsd:long' ||
|
|||
|
type === 'xsd:byte' ||
|
|||
|
type === 'xsd:unsignedByte'
|
|||
|
) {
|
|||
|
return 'number';
|
|||
|
} else if (type === 'xsd:boolean') {
|
|||
|
return 'boolean';
|
|||
|
}
|
|||
|
throw new Error("can't replace type: " + type);
|
|||
|
}
|
|||
|
|
|||
|
// 解析 schema 输出 ts 定义
|
|||
|
function convertSchema(fileName: string) {
|
|||
|
const wml = fs.readFileSync(fileName, 'utf8');
|
|||
|
|
|||
|
const schema = parseXML(wml)['xsd:schema'];
|
|||
|
|
|||
|
const result: string[] = [];
|
|||
|
|
|||
|
for (const key in schema) {
|
|||
|
if (key.startsWith('@_')) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
const value = schema[key];
|
|||
|
|
|||
|
if (Array.isArray(value)) {
|
|||
|
// 需要执行两边,第一遍是检查基础类型的基础类型
|
|||
|
for (const item of value) {
|
|||
|
const name = item['@_name'];
|
|||
|
// 目前仅处理简单类型
|
|||
|
if (name && !name.startsWith('ST_')) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
definedTypes[name] = true;
|
|||
|
if (key === 'xsd:complexType') {
|
|||
|
definedTypes[name] = true;
|
|||
|
let attributesAndElements = item['xsd:attribute'];
|
|||
|
if (attributesAndElements && !Array.isArray(attributesAndElements)) {
|
|||
|
attributesAndElements = [attributesAndElements];
|
|||
|
}
|
|||
|
|
|||
|
if (item['xsd:sequence']?.['xsd:element']) {
|
|||
|
const elements = item['xsd:sequence']?.['xsd:element'];
|
|||
|
if (elements) {
|
|||
|
if (!attributesAndElements) {
|
|||
|
attributesAndElements = [];
|
|||
|
}
|
|||
|
attributesAndElements = attributesAndElements.concat(elements);
|
|||
|
}
|
|||
|
}
|
|||
|
if (!attributesAndElements) {
|
|||
|
const complexContent = item['xsd:complexContent'];
|
|||
|
if (complexContent) {
|
|||
|
const extension = complexContent['xsd:extension'];
|
|||
|
if (extension) {
|
|||
|
const extensionAttributes = extension['xsd:attribute'];
|
|||
|
const base = extension['@_base'].replace(/s:/g, '');
|
|||
|
if (extensionAttributes) {
|
|||
|
result.push(`export type ${name} = ${base} & {`);
|
|||
|
result.push(...generateField(extensionAttributes));
|
|||
|
result.push('}\n');
|
|||
|
} else {
|
|||
|
const sequence = extension['xsd:sequence'];
|
|||
|
if (sequence) {
|
|||
|
let group = sequence['xsd:group'];
|
|||
|
let groupStr = '';
|
|||
|
if (group) {
|
|||
|
if (!Array.isArray(group)) {
|
|||
|
group = [group];
|
|||
|
}
|
|||
|
for (const g of group) {
|
|||
|
const ref = g['@_ref'].replace(/s:/g, '');
|
|||
|
groupStr += ref + ' & ';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (attributesAndElements) {
|
|||
|
result.push(
|
|||
|
`export type ${name} = ${base} & ${groupStr} {`
|
|||
|
);
|
|||
|
result.push(...generateField(attributesAndElements));
|
|||
|
result.push('}\n');
|
|||
|
} else {
|
|||
|
if (groupStr) {
|
|||
|
result.push(
|
|||
|
`export type ${name} = ${base} & ${groupStr.slice(
|
|||
|
0,
|
|||
|
-3
|
|||
|
)};`
|
|||
|
);
|
|||
|
} else {
|
|||
|
if (name !== base) {
|
|||
|
result.push(`export type ${name} = ${base};`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
if (name !== base) {
|
|||
|
result.push(`export type ${name} = ${base};`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
result.push(`export type ${name} = any;\n`);
|
|||
|
}
|
|||
|
} else {
|
|||
|
result.push(`export type ${name} = any;\n`);
|
|||
|
}
|
|||
|
} else {
|
|||
|
result.push(`export type ${name} = {`);
|
|||
|
result.push(...generateField(attributesAndElements));
|
|||
|
result.push('}\n');
|
|||
|
}
|
|||
|
} else if (key === 'xsd:simpleType') {
|
|||
|
// 简单类型有几种,
|
|||
|
const restriction = item['xsd:restriction'];
|
|||
|
if (restriction) {
|
|||
|
const base = restriction['@_base'];
|
|||
|
|
|||
|
if (base.startsWith('xsd:')) {
|
|||
|
allusedTypes[base] = true;
|
|||
|
const javascriptType = toJavaScriptType(base);
|
|||
|
if (javascriptType === 'string') {
|
|||
|
let enumeration = restriction['xsd:enumeration'];
|
|||
|
if (enumeration) {
|
|||
|
if (!Array.isArray(enumeration)) {
|
|||
|
enumeration = [enumeration];
|
|||
|
}
|
|||
|
// 如果是数字开头大概不会是 enum
|
|||
|
if (enumeration[0]['@_value'].match(/^[0-9]/)) {
|
|||
|
result.push(`export type ${name} = string;`);
|
|||
|
} else {
|
|||
|
const enumNames = [];
|
|||
|
for (const enumItem of enumeration) {
|
|||
|
const enumName = enumItem['@_value'];
|
|||
|
enumNames.push(enumName);
|
|||
|
}
|
|||
|
// 这里用 union 代替 enum,可以减少生成体积
|
|||
|
result.push(
|
|||
|
`export type ${name} = '${enumNames.join("' | '")}'`
|
|||
|
);
|
|||
|
}
|
|||
|
} else {
|
|||
|
result.push(`export type ${name} = string;`);
|
|||
|
}
|
|||
|
} else {
|
|||
|
result.push(`export type ${name} = ${javascriptType};`);
|
|||
|
}
|
|||
|
} else if (base.startsWith('s:') || base.startsWith('ST_')) {
|
|||
|
const baseType = base.replace('s:', '');
|
|||
|
allusedTypes[baseType] = true;
|
|||
|
if (name !== baseType) {
|
|||
|
result.push(`export type ${name} = ${baseType};`);
|
|||
|
}
|
|||
|
} else {
|
|||
|
throw new Error('Unknown base type: ' + base);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const union = item['xsd:union'];
|
|||
|
if (union) {
|
|||
|
const memberTypes = union['@_memberTypes'];
|
|||
|
if (memberTypes) {
|
|||
|
definedTypes[name] = true;
|
|||
|
const members = memberTypes
|
|||
|
.replace(/s:/g, '')
|
|||
|
.split(' ')
|
|||
|
.map((item: string) => {
|
|||
|
if (item.startsWith('xsd:')) {
|
|||
|
return toJavaScriptType(item);
|
|||
|
} else {
|
|||
|
return item;
|
|||
|
}
|
|||
|
});
|
|||
|
for (const member of members) {
|
|||
|
allusedTypes[member] = true;
|
|||
|
}
|
|||
|
const membersStr = members.join(' | ');
|
|||
|
if (name !== membersStr) {
|
|||
|
result.push(`export type ${name} = ${membersStr};\n`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
} else if (key === 'xsd:group') {
|
|||
|
const choice = item['xsd:choice'];
|
|||
|
if (choice) {
|
|||
|
let elements = choice['xsd:element'];
|
|||
|
if (elements) {
|
|||
|
if (!Array.isArray(elements)) {
|
|||
|
elements = [elements];
|
|||
|
}
|
|||
|
result.push(`export type ${name} = {`);
|
|||
|
for (const element of elements) {
|
|||
|
const elementName = element['@_name'];
|
|||
|
const elementType = element['@_type'];
|
|||
|
if (elementType) {
|
|||
|
allusedTypes[elementType] = true;
|
|||
|
result.push(` ${elementName}?: ${elementType};`);
|
|||
|
} else {
|
|||
|
console.warn("can't find type: " + name);
|
|||
|
}
|
|||
|
}
|
|||
|
result.push('}\n');
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
for (const key in allusedTypes) {
|
|||
|
if (key.startsWith('xsd:')) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
if (!definedTypes[key]) {
|
|||
|
console.log('Not defined: ' + key);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
fs.writeFileSync('../src/openxml/Types.ts', outputFile.join('\n'));
|