diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index c83281c0..784264a4 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4 +1,4 @@ -# skd-edit v1.19 — Atomic 1C DCS editor +# skd-edit v1.20 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -203,7 +203,15 @@ function Read-FieldProperties($fieldEl) { } } "valueType" { - # Read type info — store the raw element for now, we'll use type from parsed if overridden + # Preserve the entire OuterXml so rebuild can re-emit qualifiers + # (StringQualifiers, NumberQualifiers, DateQualifiers, etc.) that would + # otherwise be lost. Also extract Type string for type-override shorthand. + $raw = $ch.OuterXml + # .NET OuterXml re-declares xmlns on every element where the prefix is in + # scope (because the fragment is treated as standalone). Strip these since + # the parent context at insertion point already provides them. + $raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '') + $props["_rawValueType"] = $raw $typeEl = $null foreach ($gc in $ch.ChildNodes) { if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') { @@ -727,7 +735,7 @@ function Build-ValueTypeXml { if ($typeStr -eq "boolean") { $lines += "$indentxs:boolean" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr -match '^string(\((\d+)\))?$') { @@ -737,7 +745,7 @@ function Build-ValueTypeXml { $lines += "$indent`t$len" $lines += "$indent`tVariable" $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { @@ -750,7 +758,7 @@ function Build-ValueTypeXml { $lines += "$indent`t$fraction" $lines += "$indent`t$sign" $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr -match '^(date|dateTime)$') { @@ -762,26 +770,26 @@ function Build-ValueTypeXml { $lines += "$indent" $lines += "$indent`t$fractions" $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr -eq "StandardPeriod") { $lines += "$indentv8:StandardPeriod" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') { $lines += "$indentd5p1:$(Esc-Xml $typeStr)" - return $lines -join "`r`n" + return $lines -join "`n" } if ($typeStr.Contains('.')) { $lines += "$indentd5p1:$(Esc-Xml $typeStr)" - return $lines -join "`r`n" + return $lines -join "`n" } $lines += "$indent$(Esc-Xml $typeStr)" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-MLTextXml { @@ -793,7 +801,7 @@ function Build-MLTextXml { $lines += "$indent`t`t$(Esc-Xml $text)" $lines += "$indent`t" $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-RoleXml { @@ -812,7 +820,7 @@ function Build-RoleXml { } } $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-RestrictionXml { @@ -834,7 +842,7 @@ function Build-RestrictionXml { } } $lines += "$indent" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-FieldFragment { @@ -857,14 +865,18 @@ function Build-FieldFragment { $roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t" if ($roleXml) { $lines += $roleXml } - if ($parsed.type) { + if ($parsed.rawValueType) { + # Preserve original verbatim — keeps qualifiers (StringQualifiers, + # NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand. + $lines += "$i`t" + $parsed.rawValueType + } elseif ($parsed.type) { $lines += "$i`t" $lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t") $lines += "$i`t" } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-TotalFragment { @@ -876,7 +888,7 @@ function Build-TotalFragment { $lines += "$i`t$(Esc-Xml $parsed.dataPath)" $lines += "$i`t$(Esc-Xml $parsed.expression)" $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-CalcFieldFragment { @@ -903,7 +915,7 @@ function Build-CalcFieldFragment { } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-ParamValueXml { @@ -1010,7 +1022,7 @@ function Build-ParamFragment { } $lines += "$i" - $fragments += ($lines -join "`r`n") + $fragments += ($lines -join "`n") if ($parsed.autoDates) { $paramName = $parsed.name @@ -1027,7 +1039,7 @@ function Build-ParamFragment { $bLines += "$i`ttrue" $bLines += "$i`t$(Esc-Xml "&$paramName.ДатаНачала")" $bLines += "$i" - $fragments += ($bLines -join "`r`n") + $fragments += ($bLines -join "`n") $eLines = @() $eLines += "$i" @@ -1040,7 +1052,7 @@ function Build-ParamFragment { $eLines += "$i`ttrue" $eLines += "$i`t$(Esc-Xml "&$paramName.ДатаОкончания")" $eLines += "$i" - $fragments += ($eLines -join "`r`n") + $fragments += ($eLines -join "`n") } return ,$fragments @@ -1075,7 +1087,7 @@ function Build-FilterItemFragment { } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-SelectionItemFragment { @@ -1116,7 +1128,7 @@ function Build-SelectionItemFragment { $lines += "$i`t$(Esc-Xml $fieldName)" $lines += "$i" } - return $lines -join "`r`n" + return $lines -join "`n" } function Build-DataParamFragment { @@ -1158,7 +1170,7 @@ function Build-DataParamFragment { } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-OrderItemFragment { @@ -1174,7 +1186,7 @@ function Build-OrderItemFragment { $lines += "$i`t$($parsed.direction)" $lines += "$i" } - return $lines -join "`r`n" + return $lines -join "`n" } function Build-DataSetLinkFragment { @@ -1191,7 +1203,7 @@ function Build-DataSetLinkFragment { $lines += "$i`t$(Esc-Xml $parsed.parameter)" } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-DataSetQueryFragment { @@ -1204,7 +1216,7 @@ function Build-DataSetQueryFragment { $lines += "$i`t$(Esc-Xml $parsed.dataSource)" $lines += "$i`t$(Esc-Xml $parsed.query)" $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-VariantFragment { @@ -1230,7 +1242,7 @@ function Build-VariantFragment { $lines += "$i`t`t" $lines += "$i`t" $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Emit-FilterComparison { @@ -1312,7 +1324,7 @@ function Build-ConditionalAppearanceItemFragment { $lines += "$i`t" $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-StructureItemFragment { @@ -1364,7 +1376,7 @@ function Build-StructureItemFragment { } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } function Build-OutputParamFragment { @@ -1392,7 +1404,7 @@ function Build-OutputParamFragment { } $lines += "$i" - return $lines -join "`r`n" + return $lines -join "`n" } # --- 5. XML helpers --- @@ -1437,7 +1449,9 @@ function Get-ChildIndent($container) { } function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { - $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") + # LF line endings — 1С DCS files use LF consistently; CRLF causes idempotency + # leaks when modify-* removes one whitespace and inserts a different-style one. + $ws = $xmlDoc.CreateWhitespace("`n$childIndent") if ($refNode) { $container.InsertBefore($ws, $refNode) | Out-Null $container.InsertBefore($newNode, $ws) | Out-Null @@ -1450,7 +1464,7 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { $container.AppendChild($ws) | Out-Null $container.AppendChild($newNode) | Out-Null $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } - $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent") + $closeWs = $xmlDoc.CreateWhitespace("`n$parentIndent") $container.AppendChild($closeWs) | Out-Null } } @@ -1729,6 +1743,10 @@ $script:RawOriginal = [System.IO.File]::ReadAllText($resolvedPath, [System.Text. $rootOpenMatch = [regex]::Match($script:RawOriginal, ']*>') if ($rootOpenMatch.Success) { $script:RawRootOpening = $rootOpenMatch.Value } else { $script:RawRootOpening = $null } +# Detect line ending convention so save can normalize back to whatever the source used. +# 1С Designer writes CRLF on Windows; LF-edited files should stay LF. +$script:LineEnding = if ($script:RawOriginal.Contains("`r`n")) { "`r`n" } else { "`n" } + $xmlDoc = New-Object System.Xml.XmlDocument $xmlDoc.PreserveWhitespace = $true $xmlDoc.Load($resolvedPath) @@ -2021,7 +2039,7 @@ switch ($Operation) { } } $valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent - $fragXml = $valueLines -join "`r`n" + $fragXml = $valueLines -join "`n" $wasExisting = ($null -ne $existing) if ($existing) { @@ -2107,7 +2125,7 @@ switch ($Operation) { } foreach ($av in $avItems) { $avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent - $fragXml = $avLines -join "`r`n" + $fragXml = $avLines -join "`n" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent @@ -2671,7 +2689,7 @@ switch ($Operation) { $lines += "$itemIndent`t0001-01-01T00:00:00" $lines += "$itemIndent`t0001-01-01T00:00:00" $lines += "$itemIndent" - $fragXml = $lines -join "`r`n" + $fragXml = $lines -join "`n" $nodes = Import-Fragment $xmlDoc $fragXml foreach ($node in $nodes) { Insert-BeforeElement $giEl $node $null $itemIndent @@ -2992,7 +3010,7 @@ switch ($Operation) { } else { $valLines += "$itemIndent$(Esc-Xml "$($parsed.value)")" } - $valXml = $valLines -join "`r`n" + $valXml = $valLines -join "`n" $valNodes = Import-Fragment $xmlDoc $valXml foreach ($node in $valNodes) { Insert-BeforeElement $dpItem $node $null $itemIndent @@ -3056,6 +3074,9 @@ switch ($Operation) { type = if ($parsed.type) { $parsed.type } else { $existing.type } roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles } restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict } + # Preserve raw only when user did NOT override type via shorthand — + # otherwise the override path rebuilds valueType from $parsed.type. + rawValueType = if ($parsed.type) { $null } else { $existing._rawValueType } } # Remember position (NextSibling after whitespace) @@ -3143,7 +3164,7 @@ switch ($Operation) { $lines += "$fieldIndent`t$(Esc-Xml $kv[$k])" } $lines += "$fieldIndent" - $fragXml = $lines -join "`r`n" + $fragXml = $lines -join "`n" # Insert before , else before , else at end $refNode = $null @@ -3461,6 +3482,14 @@ $content = [regex]::Replace( # (``) but 1C-Designer writes ``. Strip the space. $content = [regex]::Replace($content, '(?<=\S) />', '/>') +# (4) normalize line endings to match source — operations may mix LF (from new +# fragments) with whatever the source used (CRLF on Windows, LF on Linux/git). +if ($script:LineEnding -eq "`r`n") { + $content = $content -replace '(? serialization so rebuild can re-emit qualifiers + # (StringQualifiers, NumberQualifiers, DateQualifiers, …) that aren't + # expressible via shorthand. Strip xmlns declarations that lxml re-emits when + # serializing a sub-element (parent context already provides them). + raw = etree.tostring(ch, encoding="unicode") + raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw) + props["_rawValueType"] = raw for gc in ch: if isinstance(gc.tag, str) and local_name(gc) == "Type": props["_rawTypeText"] = (gc.text or "").strip() @@ -683,7 +690,7 @@ def build_value_type_xml(type_str, indent): if type_str == "boolean": lines.append(f"{indent}xs:boolean") - return "\r\n".join(lines) + return "\n".join(lines) m = re.match(r'^string(\((\d+)\))?$', type_str) if m: @@ -693,7 +700,7 @@ def build_value_type_xml(type_str, indent): lines.append(f"{indent}\t{length}") lines.append(f"{indent}\tVariable") lines.append(f"{indent}") - return "\r\n".join(lines) + return "\n".join(lines) m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str) if m: @@ -705,7 +712,7 @@ def build_value_type_xml(type_str, indent): lines.append(f"{indent}\t{fraction}") lines.append(f"{indent}\t{sign}") lines.append(f"{indent}") - return "\r\n".join(lines) + return "\n".join(lines) m = re.match(r'^(date|dateTime)$', type_str) if m: @@ -714,22 +721,22 @@ def build_value_type_xml(type_str, indent): lines.append(f"{indent}") lines.append(f"{indent}\t{fractions}") lines.append(f"{indent}") - return "\r\n".join(lines) + return "\n".join(lines) if type_str == "StandardPeriod": lines.append(f"{indent}v8:StandardPeriod") - return "\r\n".join(lines) + return "\n".join(lines) if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.', type_str): lines.append(f'{indent}d5p1:{esc_xml(type_str)}') - return "\r\n".join(lines) + return "\n".join(lines) if "." in type_str: lines.append(f'{indent}d5p1:{esc_xml(type_str)}') - return "\r\n".join(lines) + return "\n".join(lines) lines.append(f"{indent}{esc_xml(type_str)}") - return "\r\n".join(lines) + return "\n".join(lines) def build_mltext_xml(tag, text, indent): @@ -741,7 +748,7 @@ def build_mltext_xml(tag, text, indent): f"{indent}\t", f"{indent}", ] - return "\r\n".join(lines) + return "\n".join(lines) def build_role_xml(roles, indent): @@ -755,7 +762,7 @@ def build_role_xml(roles, indent): else: lines.append(f"{indent}\ttrue") lines.append(f"{indent}") - return "\r\n".join(lines) + return "\n".join(lines) def build_restriction_xml(restrict, indent): @@ -768,7 +775,7 @@ def build_restriction_xml(restrict, indent): if xml_name: lines.append(f"{indent}\t<{xml_name}>true") lines.append(f"{indent}") - return "\r\n".join(lines) + return "\n".join(lines) def build_field_fragment(parsed, indent): @@ -787,13 +794,17 @@ def build_field_fragment(parsed, indent): if role_xml: lines.append(role_xml) - if parsed.get("type"): + if parsed.get("rawValueType"): + # Preserve original verbatim — keeps qualifiers (StringQualifiers, + # NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand. + lines.append(f"{i}\t" + parsed["rawValueType"]) + elif parsed.get("type"): lines.append(f"{i}\t") lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t")) lines.append(f"{i}\t") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_total_fragment(parsed, indent): @@ -804,7 +815,7 @@ def build_total_fragment(parsed, indent): f"{i}\t{esc_xml(parsed['expression'])}", f"{i}", ] - return "\r\n".join(lines) + return "\n".join(lines) def build_calc_field_fragment(parsed, indent): @@ -823,7 +834,7 @@ def build_calc_field_fragment(parsed, indent): lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t")) lines.append(f"{i}\t") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_param_value_xml(type_str, value, indent, tag_name="value", tag_ns=""): @@ -897,7 +908,7 @@ def build_param_fragment(parsed, indent): lines.append(f"{i}\tAlways") lines.append(f"{i}") - fragments.append("\r\n".join(lines)) + fragments.append("\n".join(lines)) if parsed.get("autoDates"): param_name = parsed["name"] @@ -914,7 +925,7 @@ def build_param_fragment(parsed, indent): f"{i}\t{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}", f"{i}", ] - fragments.append("\r\n".join(b_lines)) + fragments.append("\n".join(b_lines)) e_lines = [ f"{i}", @@ -928,7 +939,7 @@ def build_param_fragment(parsed, indent): f"{i}\t{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}", f"{i}", ] - fragments.append("\r\n".join(e_lines)) + fragments.append("\n".join(e_lines)) return fragments @@ -955,7 +966,7 @@ def build_filter_item_fragment(parsed, indent): lines.append(f"{i}\t{esc_xml(uid)}") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_selection_item_fragment(field_name, indent): @@ -986,13 +997,13 @@ def build_selection_item_fragment(field_name, indent): lines.append(f"{i}\t") lines.append(f"{i}\tAuto") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) lines = [ f'{i}', f"{i}\t{esc_xml(field_name)}", f"{i}", ] - return "\r\n".join(lines) + return "\n".join(lines) def build_data_param_fragment(parsed, indent): @@ -1027,7 +1038,7 @@ def build_data_param_fragment(parsed, indent): lines.append(f"{i}\t{esc_xml(uid)}") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_order_item_fragment(parsed, indent): @@ -1040,7 +1051,7 @@ def build_order_item_fragment(parsed, indent): f"{i}\t{parsed['direction']}", f"{i}", ] - return "\r\n".join(lines) + return "\n".join(lines) def build_data_set_link_fragment(parsed, indent): @@ -1055,7 +1066,7 @@ def build_data_set_link_fragment(parsed, indent): if parsed.get("parameter"): lines.append(f"{i}\t{esc_xml(parsed['parameter'])}") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_data_set_query_fragment(parsed, indent): @@ -1067,7 +1078,7 @@ def build_data_set_query_fragment(parsed, indent): f"{i}\t{esc_xml(parsed['query'])}", f"{i}", ] - return "\r\n".join(lines) + return "\n".join(lines) def build_variant_fragment(parsed, indent): @@ -1092,7 +1103,7 @@ def build_variant_fragment(parsed, indent): f"{i}\t", f"{i}", ] - return "\r\n".join(lines) + return "\n".join(lines) def _emit_filter_comparison(lines, f, indent): @@ -1159,7 +1170,7 @@ def build_conditional_appearance_item_fragment(parsed, indent): lines.append(f"{i}\t") lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_structure_item_fragment(item, indent): @@ -1196,7 +1207,7 @@ def build_structure_item_fragment(item, indent): lines.append(build_structure_item_fragment(child, f"{i}\t")) lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) def build_output_param_fragment(parsed, indent): @@ -1219,7 +1230,7 @@ def build_output_param_fragment(parsed, indent): lines.append(f'{i}\t{esc_xml(val)}') lines.append(f"{i}") - return "\r\n".join(lines) + return "\n".join(lines) # ── 5. XML helpers ────────────────────────────────────────── @@ -1505,6 +1516,9 @@ raw_original_text = raw_original_bytes.lstrip(b"\xef\xbb\xbf").decode("utf-8") _root_open_m = re.search(r"]*>", raw_original_text, re.DOTALL) raw_root_opening = _root_open_m.group(0) if _root_open_m else None +# Detect line ending convention so save can normalize back to whatever the source used. +line_ending = "\r\n" if "\r\n" in raw_original_text else "\n" + xml_parser = etree.XMLParser(remove_blank_text=False) tree = etree.parse(resolved_path, xml_parser) xml_doc = tree.getroot() @@ -1739,7 +1753,7 @@ elif operation == "modify-parameter": declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip()) break value_lines = build_param_value_xml(declared_type, value, child_indent) - frag_xml = "\r\n".join(value_lines) + frag_xml = "\n".join(value_lines) was_existing = existing is not None if existing is not None: # Find next-element sibling as ref before removing @@ -1798,7 +1812,7 @@ elif operation == "modify-parameter": break for av in av_items: av_lines = build_available_value_fragment(av, declared_type, child_indent) - frag_xml = "\r\n".join(av_lines) + frag_xml = "\n".join(av_lines) nodes = import_fragment(xml_doc, frag_xml) for node in nodes: insert_before_element(param_el, node, ref_node, child_indent) @@ -2223,7 +2237,7 @@ elif operation == "modify-structure": f'{item_indent}\t0001-01-01T00:00:00', f'{item_indent}', ] - frag_xml = "\r\n".join(lines) + frag_xml = "\n".join(lines) for node in import_fragment(xml_doc, frag_xml): insert_before_element(gi_el, node, None, item_indent) @@ -2486,7 +2500,7 @@ elif operation == "modify-dataParameter": else: val_lines.append(f'{item_indent}{esc_xml(str(pv))}') - val_xml = "\r\n".join(val_lines) + val_xml = "\n".join(val_lines) val_nodes = import_fragment(xml_doc, val_xml) for node in val_nodes: insert_before_element(dp_item, node, None, item_indent) @@ -2532,6 +2546,8 @@ elif operation == "modify-field": "type": parsed["type"] if parsed.get("type") else existing["type"], "roles": parsed["roles"] if parsed.get("roles") else existing["roles"], "restrict": parsed["restrict"] if parsed.get("restrict") else existing["restrict"], + # Preserve raw only when user did NOT override type via shorthand. + "rawValueType": None if parsed.get("type") else existing.get("_rawValueType"), } # Find next element sibling for position @@ -2605,7 +2621,7 @@ elif operation == "set-field-role": for k, v in kv: lines.append(f"{field_indent}\t{esc_xml(v)}") lines.append(f"{field_indent}") - frag_xml = "\r\n".join(lines) + frag_xml = "\n".join(lines) ref_node = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) in ("valueType", "inputParameters") and etree.QName(ch.tag).namespace == SCH_NS), None) for node in import_fragment(xml_doc, frag_xml): @@ -2870,6 +2886,12 @@ xml_text = re.sub( # Normalize self-closing tags: lxml writes `` already (no space), but be # defensive — strip any space before `/>` so PS and PY ports stay byte-equivalent. xml_text = re.sub(r"(?<=\S) />", "/>", xml_text) + +# Normalize line endings to match source. +if line_ending == "\r\n": + xml_text = re.sub(r"(?