@dialecte/create 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +88 -0
  2. package/dist/cli/index.js +281 -0
  3. package/package.json +43 -0
  4. package/python/generate/__init__.py +1 -0
  5. package/python/generate/__main__.py +103 -0
  6. package/python/generate/collector.py +128 -0
  7. package/python/generate/deriver.py +117 -0
  8. package/python/generate/emitters/__init__.py +1 -0
  9. package/python/generate/emitters/constants.py +69 -0
  10. package/python/generate/emitters/definition.py +49 -0
  11. package/python/generate/emitters/ts_helpers.py +283 -0
  12. package/python/generate/emitters/types.py +67 -0
  13. package/python/generate/extractors/__init__.py +1 -0
  14. package/python/generate/extractors/attributes.py +83 -0
  15. package/python/generate/extractors/children.py +175 -0
  16. package/python/generate/extractors/constraints.py +154 -0
  17. package/python/generate/extractors/docs.py +32 -0
  18. package/python/generate/extractors/facets.py +168 -0
  19. package/python/generate/extractors/namespace.py +59 -0
  20. package/python/generate/globals.py +99 -0
  21. package/python/generate/helpers.py +143 -0
  22. package/python/generate/ir.py +81 -0
  23. package/python/generate/orphans.py +69 -0
  24. package/python/generate/xpath_parser.py +167 -0
  25. package/python/generate/xsi_type.py +150 -0
  26. package/python/pyproject.toml +15 -0
  27. package/templates/dialecte/README.md +39 -0
  28. package/templates/dialecte/_gitignore +4 -0
  29. package/templates/dialecte/docs/.vitepress/config.ts +15 -0
  30. package/templates/dialecte/docs/index.md +18 -0
  31. package/templates/dialecte/env.d.ts +1 -0
  32. package/templates/dialecte/package.json +45 -0
  33. package/templates/dialecte/src/__version__/config/dialecte.config.ts +48 -0
  34. package/templates/dialecte/src/__version__/config/hydrated.types.ts +78 -0
  35. package/templates/dialecte/src/__version__/config/index.ts +2 -0
  36. package/templates/dialecte/src/__version__/config/namespaces.ts +6 -0
  37. package/templates/dialecte/src/__version__/definition/.gitkeep +2 -0
  38. package/templates/dialecte/src/__version__/definition/index.ts +4 -0
  39. package/templates/dialecte/src/__version__/dialecte.ts +30 -0
  40. package/templates/dialecte/src/__version__/extensions/index.ts +3 -0
  41. package/templates/dialecte/src/__version__/index.ts +2 -0
  42. package/templates/dialecte/src/__version__/test/hydrated-test.ts +53 -0
  43. package/templates/dialecte/src/__version__/test/index.ts +1 -0
  44. package/templates/dialecte/src/index.ts +1 -0
  45. package/templates/dialecte/tsconfig.build.json +24 -0
  46. package/templates/dialecte/tsconfig.json +8 -0
  47. package/templates/dialecte/tsconfig.node.json +13 -0
  48. package/templates/dialecte/tsconfig.vitest.json +10 -0
  49. package/templates/dialecte/vite.config.ts +36 -0
  50. package/templates/dialecte/vitest.config.ts +35 -0
  51. package/vendor/elementpath-5.1.1-py3-none-any.whl +0 -0
  52. package/vendor/xmlschema-4.3.1-py3-none-any.whl +0 -0
@@ -0,0 +1,143 @@
1
+ """Shared utility functions for the generation pipeline."""
2
+ import re
3
+ import unicodedata
4
+ from typing import Any
5
+
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # XSD pattern → JS regex conversion
9
+ # ---------------------------------------------------------------------------
10
+
11
+ # XSD Unicode block names → (start, end) code-point ranges.
12
+ # Covers all blocks that appear in IEC 61850 XSDs and common XML XSD patterns.
13
+ # Source: Unicode 15.1 block ranges.
14
+ _UNICODE_BLOCKS: dict[str, tuple[int, int]] = {
15
+ 'IsBasicLatin': (0x0000, 0x007F),
16
+ 'IsLatin-1Supplement': (0x0080, 0x00FF),
17
+ 'IsLatinExtended-A': (0x0100, 0x017F),
18
+ 'IsLatinExtended-B': (0x0180, 0x024F),
19
+ 'IsIPAExtensions': (0x0250, 0x02AF),
20
+ 'IsSpacingModifierLetters': (0x02B0, 0x02FF),
21
+ 'IsCombiningDiacriticalMarks': (0x0300, 0x036F),
22
+ 'IsGreek': (0x0370, 0x03FF),
23
+ 'IsCyrillic': (0x0400, 0x04FF),
24
+ 'IsArmenian': (0x0530, 0x058F),
25
+ 'IsHebrew': (0x0590, 0x05FF),
26
+ 'IsArabic': (0x0600, 0x06FF),
27
+ 'IsThai': (0x0E00, 0x0E7F),
28
+ 'IsHiragana': (0x3040, 0x309F),
29
+ 'IsKatakana': (0x30A0, 0x30FF),
30
+ 'IsCJKUnifiedIdeographs': (0x4E00, 0x9FFF),
31
+ }
32
+
33
+
34
+ def _block_range_escape(start: int, end: int) -> str:
35
+ """Emit a JS-safe character range string like ``\\x00-\\x7F``."""
36
+ def _esc(cp: int) -> str:
37
+ if cp <= 0xFF:
38
+ return f'\\x{cp:02x}'
39
+ if cp <= 0xFFFF:
40
+ return f'\\u{cp:04x}'
41
+ return f'\\u{{{cp:x}}}'
42
+ return f'{_esc(start)}-{_esc(end)}'
43
+
44
+
45
+ def xsd_pattern_to_js(pattern: str) -> str:
46
+ """Convert an XSD regular expression to a JS-compatible regex string.
47
+
48
+ Handles:
49
+ - ``\\i`` / ``\\I`` (XML initial name char class)
50
+ - ``\\c`` / ``\\C`` (XML name char class)
51
+ - ``\\p{BlockName}`` → Unicode code-point range
52
+ - ``&#xHHHH;`` and ``&#DDD;`` XML character references
53
+ """
54
+ result = pattern
55
+
56
+ # \i → XML initial name character (letter, underscore, colon)
57
+ result = result.replace(r'\i', '[A-Za-z_:]')
58
+ # \I → complement
59
+ result = result.replace(r'\I', '[^A-Za-z_:]')
60
+
61
+ # \c → XML name character (letter, digit, dot, dash, underscore, colon)
62
+ result = result.replace(r'\c', '[-.:0-9A-Z_a-z]')
63
+ # \C → complement
64
+ result = result.replace(r'\C', '[^-.:0-9A-Z_a-z]')
65
+
66
+ # \p{BlockName} → code-point range
67
+ def _replace_block(m: re.Match[str]) -> str:
68
+ name = m.group(1)
69
+ rng = _UNICODE_BLOCKS.get(name)
70
+ if rng:
71
+ return _block_range_escape(*rng)
72
+ # Unknown block — leave as-is (will fail in JS, intentionally loud)
73
+ return m.group(0)
74
+
75
+ result = re.sub(r'\\p\{([^}]+)\}', _replace_block, result)
76
+
77
+ # XML hex character reference &#xHHHH;
78
+ result = re.sub(
79
+ r'&#x([0-9A-Fa-f]+);',
80
+ lambda m: chr(int(m.group(1), 16)),
81
+ result,
82
+ )
83
+ # XML decimal character reference &#DDD;
84
+ result = re.sub(
85
+ r'&#(\d+);',
86
+ lambda m: chr(int(m.group(1), 10)),
87
+ result,
88
+ )
89
+
90
+ return result
91
+ def local_name(qname: str) -> str:
92
+ """Extract local name from a Clark-notation QName like '{uri}local' or 'prefix:local'."""
93
+ if qname.startswith('{'):
94
+ return qname.split('}', 1)[-1]
95
+ if ':' in qname:
96
+ return qname.split(':', 1)[-1]
97
+ return qname
98
+ def namespace_uri(qname: str) -> str | None:
99
+ """Extract namespace URI from Clark-notation QName '{uri}local'. Returns None if absent."""
100
+ if qname.startswith('{'):
101
+ return qname[1:].split('}', 1)[0]
102
+ return None
103
+ def tokenize_xpath(xpath: str) -> list[str]:
104
+ """Split an XPath selector into its element path tokens.
105
+
106
+ Strips namespace prefixes and leading './'.
107
+ Examples:
108
+ 'tNS:LNode' -> ['LNode']
109
+ './/SubNetwork' -> ['SubNetwork']
110
+ 'Bay/ConductingEquipment' -> ['Bay', 'ConductingEquipment']
111
+ """
112
+ if not xpath:
113
+ return []
114
+ # Remove leading ./ or .//
115
+ cleaned = re.sub(r'^\.//?', '', xpath)
116
+ parts = cleaned.split('/')
117
+ tokens = []
118
+ for part in parts:
119
+ part = part.strip()
120
+ if not part or part == '.':
121
+ continue
122
+ # Strip namespace prefix
123
+ tokens.append(local_name(part))
124
+ return tokens
125
+ def get_facet_value(facet: Any) -> Any:
126
+ """Extract a scalar value from an xmlschema facet object.
127
+
128
+ Tries multiple access patterns that xmlschema uses across versions.
129
+ """
130
+ for attr in ('value', 'v', 'min_value', 'max_value'):
131
+ val = getattr(facet, attr, None)
132
+ if val is not None:
133
+ return val
134
+ # Fallback: try elem attribute
135
+ elem = getattr(facet, 'elem', None)
136
+ if elem is not None:
137
+ raw = elem.get('value')
138
+ if raw is not None:
139
+ try:
140
+ return int(raw)
141
+ except (ValueError, TypeError):
142
+ return raw
143
+ return None
@@ -0,0 +1,81 @@
1
+ """Internal Representation — Python dataclasses for XSD schema data.
2
+
3
+ Never serialized directly. Used as the in-memory model between
4
+ Parse → Collect → Derive → Emit phases.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from generate.xpath_parser import FieldPath, SelectorPath
13
+ @dataclass
14
+ class Namespace:
15
+ prefix: str
16
+ uri: str
17
+ @dataclass
18
+ class Facets:
19
+ enumeration: list[str] | None = None
20
+ pattern: list[str] | None = None
21
+ min_length: int | None = None
22
+ max_length: int | None = None
23
+ length: int | None = None
24
+ min_inclusive: int | str | None = None
25
+ max_inclusive: int | str | None = None
26
+ min_exclusive: int | str | None = None
27
+ max_exclusive: int | str | None = None
28
+ total_digits: int | None = None
29
+ fraction_digits: int | None = None
30
+ white_space: str | None = None # 'preserve' | 'replace' | 'collapse'
31
+
32
+ def is_empty(self) -> bool:
33
+ return all(v is None for v in vars(self).values())
34
+ @dataclass
35
+ class AttributeDef:
36
+ required: bool = False
37
+ default: str | None = None
38
+ fixed: str | None = None
39
+ namespace: Namespace | None = None
40
+ facets: Facets | None = None
41
+ @dataclass
42
+ class IdentityConstraint:
43
+ kind: str # 'unique' | 'key' | 'keyref'
44
+ name: str
45
+ selector: list[SelectorPath] # parsed XPath alternatives (union)
46
+ fields: list[FieldPath] # parsed field XPath expressions
47
+ deep: bool = False
48
+ refer: str | None = None # keyref only
49
+
50
+ @dataclass
51
+ class ChildDef:
52
+ required: bool = False
53
+ min_occurs: int = 0
54
+ max_occurs: int | None = None # None = unbounded
55
+ constraints: list[IdentityConstraint] | None = None
56
+ facets: Facets | None = None
57
+
58
+ @dataclass
59
+ class ChoiceGroup:
60
+ options: list[str]
61
+ min_occurs: int = 0
62
+ max_occurs: int | None = None
63
+ @dataclass
64
+ class TextContent:
65
+ facets: Facets | None = None
66
+ @dataclass
67
+ class ElementDef:
68
+ tag: str
69
+ namespace: Namespace
70
+ documentation: str | None = None
71
+ parents: list[str] = field(default_factory=list)
72
+ attr_sequence: list[str] = field(default_factory=list)
73
+ attr_any: bool = False
74
+ attributes: dict[str, AttributeDef] = field(default_factory=dict)
75
+ child_sequence: list[str] = field(default_factory=list)
76
+ child_any: bool = False
77
+ children: dict[str, ChildDef] = field(default_factory=dict)
78
+ choices: list[ChoiceGroup] = field(default_factory=list)
79
+ constraints: list[IdentityConstraint] = field(default_factory=list)
80
+ text_content: TextContent | None = None
81
+ identity_fields: list[str] = field(default_factory=list)
@@ -0,0 +1,69 @@
1
+ """Orphan element detection and parent injection from a JSON sidecar mapping.
2
+
3
+ Inserted between Collect and Derive phases. Handles XSD global elements that
4
+ are children-via-xs:any wildcards (e.g. IEC 61850-6-100 extension elements)
5
+ whose parent-child relationships are domain knowledge, not XSD-declared.
6
+ """
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from generate.ir import ChildDef, ElementDef
11
+
12
+
13
+ def load_parent_mapping(entry_path: Path) -> dict[str, list[str]]:
14
+ """Read parent-mapping.json from the same directory as the entry XSD.
15
+
16
+ Returns empty dict if the file does not exist.
17
+ """
18
+ mapping_file = entry_path.parent / 'parent-mapping.json'
19
+ if not mapping_file.exists():
20
+ return {}
21
+ with open(mapping_file) as f:
22
+ return json.load(f)
23
+
24
+
25
+ def detect_orphans(elements: dict[str, ElementDef], root_name: str) -> list[str]:
26
+ """Return parentless element names, excluding the root element."""
27
+ return sorted(
28
+ name for name, e in elements.items()
29
+ if not e.parents and name != root_name
30
+ )
31
+
32
+
33
+ def inject_orphan_parents(
34
+ elements: dict[str, ElementDef],
35
+ mapping: dict[str, list[str]],
36
+ root_name: str = '',
37
+ ) -> list[str]:
38
+ """Inject parent-child links from the sidecar mapping.
39
+
40
+ For each mapped orphan:
41
+ - Sets element.parents to the mapped parent list
42
+ - Adds element to each parent's children dict and child_sequence
43
+
44
+ *root_name* is excluded from orphan detection.
45
+ Returns names of orphan elements NOT covered by the mapping (for warnings).
46
+ """
47
+ orphan_names = {name for name, e in elements.items() if not e.parents and name != root_name}
48
+ mapped_orphans = orphan_names & mapping.keys()
49
+ unmapped = sorted(orphan_names - mapping.keys())
50
+
51
+ for name in sorted(mapped_orphans):
52
+ parents = mapping[name]
53
+
54
+ elem = elements[name]
55
+ valid_parents = [p for p in parents if p in elements]
56
+ elem.parents = valid_parents
57
+
58
+ for parent_name in valid_parents:
59
+ parent = elements[parent_name]
60
+ if name not in parent.children:
61
+ parent.children[name] = ChildDef(
62
+ required=False,
63
+ min_occurs=0,
64
+ max_occurs=None,
65
+ )
66
+ if name not in parent.child_sequence:
67
+ parent.child_sequence.append(name)
68
+
69
+ return unmapped
@@ -0,0 +1,167 @@
1
+ """Parse XSD identity-constraint XPath selectors and fields (§3.11.6).
2
+
3
+ Implements the restricted grammar from XSD 1.1 Part 1, §3.11.6.2 (selectors)
4
+ and §3.11.6.3 (fields):
5
+
6
+ Selector ::= Path ( '|' Path )*
7
+ Path ::= ('.' '//')? Step ( '/' Step )*
8
+ Step ::= '.' | NameTest
9
+ NameTest ::= QName | '*' | NCName ':*'
10
+
11
+ Field variant: last step can also be ``@ NameTest``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from typing import Literal
19
+
20
+ from generate.helpers import local_name
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # IR dataclasses
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @dataclass(frozen=True)
28
+ class XPathStep:
29
+ """A single step in a selector/field path.
30
+
31
+ kind:
32
+ - 'self' → the literal '.'
33
+ - 'name' → local element name (ns stripped)
34
+ - 'wildcard' → '*'
35
+ - 'ns-wildcard' → 'NCName:*' (value = NCName)
36
+ """
37
+ kind: Literal['self', 'name', 'wildcard', 'ns-wildcard']
38
+ value: str | None = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class SelectorPath:
43
+ """One alternative of a selector (before/after '|')."""
44
+ deep: bool = False
45
+ steps: tuple[XPathStep, ...] = ()
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class FieldTarget:
50
+ """The terminal target of a field XPath — element or attribute.
51
+
52
+ kind:
53
+ - 'element' → child element name
54
+ - 'attribute' → @attrName
55
+ - 'wildcard' → * or @*
56
+ - 'ns-wildcard' → NCName:* or @NCName:*
57
+ """
58
+ kind: Literal['element', 'attribute', 'wildcard', 'ns-wildcard']
59
+ value: str | None = None
60
+ is_attribute: bool = False
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class FieldPath:
65
+ """Parsed XPath field expression."""
66
+ deep: bool = False
67
+ steps: tuple[XPathStep, ...] = ()
68
+ target: FieldTarget = field(default_factory=lambda: FieldTarget(kind='wildcard'))
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Parser
73
+ # ---------------------------------------------------------------------------
74
+
75
+ _PIPE_RE = re.compile(r'\s*\|\s*')
76
+
77
+
78
+ def parse_selector(xpath: str) -> list[SelectorPath]:
79
+ """Parse a selector XPath into a list of SelectorPath alternatives."""
80
+ if not xpath or not xpath.strip():
81
+ return []
82
+ alternatives = _PIPE_RE.split(xpath.strip())
83
+ return [_parse_selector_path(alt.strip()) for alt in alternatives if alt.strip()]
84
+
85
+
86
+ def parse_field(xpath: str) -> FieldPath:
87
+ """Parse a field XPath into a FieldPath."""
88
+ if not xpath or not xpath.strip():
89
+ return FieldPath()
90
+ return _parse_field_path(xpath.strip())
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Internal
95
+ # ---------------------------------------------------------------------------
96
+
97
+ def _parse_selector_path(raw: str) -> SelectorPath:
98
+ """Parse one selector alternative (no '|')."""
99
+ deep, remainder = _consume_deep_prefix(raw)
100
+ tokens = _split_steps(remainder)
101
+ steps = tuple(_parse_step(t) for t in tokens)
102
+ return SelectorPath(deep=deep, steps=steps)
103
+
104
+
105
+ def _parse_field_path(raw: str) -> FieldPath:
106
+ """Parse a field XPath expression."""
107
+ deep, remainder = _consume_deep_prefix(raw)
108
+ tokens = _split_steps(remainder)
109
+
110
+ if not tokens:
111
+ return FieldPath(deep=deep)
112
+
113
+ # Check if last token is an attribute reference
114
+ last = tokens[-1]
115
+ if last.startswith('@'):
116
+ steps = tuple(_parse_step(t) for t in tokens[:-1])
117
+ target = _parse_field_target(last, is_attribute=True)
118
+ else:
119
+ steps = tuple(_parse_step(t) for t in tokens[:-1])
120
+ target = _parse_field_target(last, is_attribute=False)
121
+
122
+ return FieldPath(deep=deep, steps=steps, target=target)
123
+
124
+
125
+ def _consume_deep_prefix(raw: str) -> tuple[bool, str]:
126
+ """Strip leading './' or './/' and return (deep, remainder)."""
127
+ if raw.startswith('.//'):
128
+ return True, raw[3:]
129
+ if raw.startswith('./'):
130
+ return False, raw[2:]
131
+ return False, raw
132
+
133
+
134
+ def _split_steps(raw: str) -> list[str]:
135
+ """Split path into step tokens on '/' boundaries."""
136
+ if not raw:
137
+ return []
138
+ parts = raw.split('/')
139
+ return [p.strip() for p in parts if p.strip()]
140
+
141
+
142
+ def _parse_step(token: str) -> XPathStep:
143
+ """Parse a single step token into an XPathStep."""
144
+ if token == '.':
145
+ return XPathStep(kind='self')
146
+ if token == '*':
147
+ return XPathStep(kind='wildcard')
148
+ if token.endswith(':*'):
149
+ prefix = token[:-2]
150
+ return XPathStep(kind='ns-wildcard', value=prefix)
151
+ # QName — strip namespace prefix
152
+ return XPathStep(kind='name', value=local_name(token))
153
+
154
+
155
+ def _parse_field_target(token: str, *, is_attribute: bool) -> FieldTarget:
156
+ """Parse the terminal token of a field path."""
157
+ raw = token.lstrip('@').strip()
158
+
159
+ if raw == '*':
160
+ return FieldTarget(kind='wildcard', is_attribute=is_attribute)
161
+ if raw.endswith(':*'):
162
+ return FieldTarget(kind='ns-wildcard', value=raw[:-2], is_attribute=is_attribute)
163
+ return FieldTarget(
164
+ kind='attribute' if is_attribute else 'element',
165
+ value=local_name(raw),
166
+ is_attribute=is_attribute,
167
+ )
@@ -0,0 +1,150 @@
1
+ """xsi:type expansion — resolve XSD type-derivation polymorphism into the IR.
2
+
3
+ Some schemas (notably IEC 61131-10) model their content polymorphically: a named
4
+ element slot is declared with an **abstract** complex type, and instances select a
5
+ concrete variant at runtime via the ``xsi:type`` attribute. For example::
6
+
7
+ <BodyContent xsi:type="FBD"><Network>…</Network></BodyContent>
8
+
9
+ Here ``BodyContent`` is the element name; ``FBD`` is a *type* selected via
10
+ ``xsi:type``. The concrete forms (``FBD``, ``Block``, ``DataSource``, …) have **no**
11
+ global element declarations, so the element-instance-driven collector never reaches
12
+ their children.
13
+
14
+ This module reverses the XSD type-derivation graph (``schema.maps.types``) to find,
15
+ for any declared type, the concrete types that may substitute it. Their children and
16
+ attributes are unioned into the slot element (as optional members, since each is only
17
+ required for a specific ``xsi:type``), and an ``xsi:type`` enumeration attribute is
18
+ added listing the valid variant names.
19
+
20
+ This expansion runs automatically for every schema. For slots declared with a
21
+ concrete (non-abstract) type that has no derived variants, ``_concrete_substitution_types``
22
+ returns ``[]`` and ``expand`` is a no-op — zero overhead. Schemas may freely mix
23
+ abstract-typed slots (expanded) with concrete-typed slots (untouched).
24
+ """
25
+ from typing import Any
26
+
27
+ from generate.extractors.attributes import extract_attributes
28
+ from generate.extractors.children import extract_children_from_type, iter_type_child_elements
29
+ from generate.helpers import local_name
30
+ from generate.ir import AttributeDef, ChildDef, Facets, Namespace
31
+
32
+ XSI_NAMESPACE = Namespace(prefix='xsi', uri='http://www.w3.org/2001/XMLSchema-instance')
33
+ XSI_TYPE_KEY = 'xsi:type'
34
+
35
+
36
+ class XsiTypeExpander:
37
+ """Expands abstract-typed element slots with their xsi:type substitution variants."""
38
+
39
+ def __init__(self, schema: Any) -> None:
40
+ self._derived = _build_derived_map(schema)
41
+
42
+ def expand(
43
+ self,
44
+ xsd_elem: Any,
45
+ attr_seq: list[str],
46
+ attrs: dict[str, AttributeDef],
47
+ child_seq: list[str],
48
+ children: dict[str, ChildDef],
49
+ ) -> None:
50
+ """Union substitution-variant children/attributes into an element's IR (in place).
51
+
52
+ Idempotent and additive: existing entries are never overwritten or removed.
53
+ Safe to call multiple times for the same element name (e.g. when the same slot
54
+ is declared in several parents with different abstract base types).
55
+ """
56
+ types = self._concrete_substitution_types(getattr(xsd_elem, 'type', None))
57
+ if not types:
58
+ return
59
+
60
+ for variant in types:
61
+ v_seq, _v_any, v_details = extract_children_from_type(variant)
62
+ for name in v_seq:
63
+ if name in children:
64
+ continue
65
+ detail = v_details[name]
66
+ children[name] = ChildDef(
67
+ required=False,
68
+ min_occurs=0,
69
+ max_occurs=detail.max_occurs,
70
+ constraints=detail.constraints,
71
+ facets=detail.facets,
72
+ )
73
+ child_seq.append(name)
74
+
75
+ for variant in types:
76
+ a_seq, _a_any, a_details = extract_attributes(variant)
77
+ for key in a_seq:
78
+ if key in attrs:
79
+ continue
80
+ detail = a_details[key]
81
+ attrs[key] = AttributeDef(
82
+ required=False,
83
+ default=detail.default,
84
+ fixed=detail.fixed,
85
+ namespace=detail.namespace,
86
+ facets=detail.facets,
87
+ )
88
+ attr_seq.append(key)
89
+
90
+ variant_names = sorted(local_name(getattr(t, 'name', '') or '') for t in types)
91
+ if variant_names and XSI_TYPE_KEY not in attrs:
92
+ attrs[XSI_TYPE_KEY] = AttributeDef(
93
+ required=False,
94
+ namespace=XSI_NAMESPACE,
95
+ facets=Facets(enumeration=variant_names),
96
+ )
97
+ attr_seq.append(XSI_TYPE_KEY)
98
+
99
+ # Keep attribute order consistent with extract_attributes (alphabetical).
100
+ attr_seq.sort()
101
+
102
+ def iter_variant_child_elements(self, xsd_elem: Any):
103
+ """Yield XsdElement children contributed by an element's substitution variants.
104
+
105
+ Used by the collector to recurse into elements that are only reachable through
106
+ xsi:type substitution (e.g. ``Network`` under an abstract ``BodyContent`` slot).
107
+ """
108
+ for variant in self._concrete_substitution_types(getattr(xsd_elem, 'type', None)):
109
+ yield from iter_type_child_elements(variant)
110
+
111
+ def _concrete_substitution_types(self, xsd_type: Any) -> list[Any]:
112
+ """Transitive concrete types derivable from *xsd_type* (BFS through abstract bases)."""
113
+ if xsd_type is None:
114
+ return []
115
+ result: list[Any] = []
116
+ seen: set[int] = set()
117
+ queue: list[Any] = [xsd_type]
118
+ while queue:
119
+ current = queue.pop(0)
120
+ for derived in self._derived.get(id(current), []):
121
+ if id(derived) in seen:
122
+ continue
123
+ seen.add(id(derived))
124
+ queue.append(derived)
125
+ if not getattr(derived, 'abstract', False):
126
+ result.append(derived)
127
+ result.sort(key=lambda t: local_name(getattr(t, 'name', '') or ''))
128
+ return result
129
+
130
+
131
+ def _build_derived_map(schema: Any) -> dict[int, list[Any]]:
132
+ """Map ``id(base_type)`` → directly-derived complex types across all schemas.
133
+
134
+ xmlschema API:
135
+ XMLSchema.maps.types: dict[str, XsdType] — every global type in the schema set
136
+ XsdComplexType.base_type: XsdType | None
137
+ XsdComplexType.derivation: 'extension' | 'restriction' | None
138
+ """
139
+ derived: dict[int, list[Any]] = {}
140
+ types = getattr(getattr(schema, 'maps', None), 'types', None)
141
+ if not types:
142
+ return derived
143
+ for xsd_type in types.values():
144
+ base = getattr(xsd_type, 'base_type', None)
145
+ if base is None:
146
+ continue
147
+ if getattr(xsd_type, 'derivation', None) not in ('extension', 'restriction'):
148
+ continue
149
+ derived.setdefault(id(base), []).append(xsd_type)
150
+ return derived
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "dialecte-generate"
3
+ version = "0.1.0"
4
+ description = "XSD to TypeScript definition generator for Dialecte"
5
+ requires-python = ">=3.13"
6
+ dependencies = ["xmlschema>=4.3.1"]
7
+
8
+ [project.optional-dependencies]
9
+ dev = ["pytest>=9.0.2"]
10
+
11
+ [project.scripts]
12
+ dialecte-generate = "generate.__main__:main"
13
+
14
+ [tool.pytest.ini_options]
15
+ testpaths = ["tests"]
@@ -0,0 +1,39 @@
1
+ # **packageName**
2
+
3
+ Dialecte SDK for `__dialecteId__`.
4
+
5
+ Built on [`@dialecte/core`](https://github.com/dialecte/core) and scaffolded with
6
+ [`@dialecte/create`](https://github.com/dialecte/create).
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install __packageName__ @dialecte/core
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { create__DialecteName__Project } from '__packageName__/__version__'
18
+
19
+ const project = create__DialecteName__Project()
20
+ await project.open('my-project')
21
+ ```
22
+
23
+ ## Regenerate definitions
24
+
25
+ The element definitions in `src/__version__/definition/` are generated from an XSD schema.
26
+ To regenerate after a schema change:
27
+
28
+ ```sh
29
+ npm create @dialecte generate -- --entry ./path/to/schema.xsd --out-dir ./src/__version__/definition
30
+ ```
31
+
32
+ ## Develop
33
+
34
+ ```sh
35
+ npm install
36
+ npm run build # type-check + bundle (ESM + d.ts)
37
+ npm test # vitest (browser)
38
+ npm run doc:dev # vitepress docs
39
+ ```
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitepress'
2
+
3
+ export default defineConfig({
4
+ title: '__packageName__',
5
+ description: 'Dialecte SDK for __dialecteId__',
6
+ themeConfig: {
7
+ nav: [{ text: 'Guide', link: '/' }],
8
+ sidebar: [
9
+ {
10
+ text: 'Introduction',
11
+ items: [{ text: 'Getting started', link: '/' }],
12
+ },
13
+ ],
14
+ },
15
+ })