fix(kyberforge): carry apostrophes verbatim through a |- literal block
The flattener's last-resort branch rewrote ASCII ' to U+2019, justified as the one combination no YAML scalar can carry verbatim. That claim was false: a |- literal block with a single indented content line carries ', ", \ and ": " verbatim and keeps text.frontmatter.description matching — as the wrapper's own docstring already said of literal blocks. The rewrite fired on 12 of 54 in-scope files, silently disabling every rule whose token contains an apostrophe. Case 20 pinned only that the scope stayed alive, so it passed either way. The emission site now splits the emitted scalar on its first newline so a carried-over trailing comment stays on the "description: |-" header line rather than becoming part of the value, and pads by span_lines - 1 - newlines. The pad stays non-negative because the branch is only reachable when the original span is at least two lines. Verified across all 73 in-scope files: no line-count changes, and exactly the 12 expected files take the new branch. One reported position moves: an alert on a description that is itself flagged shifts from the key line to the block's content line, both inside the original span. YAML cannot put a literal block's content on the key's own line, so this is unavoidable; no line at or after the end of any description span moves. Also: --output no longer absolutises the built-in style names line, JSON and CLI, which a same-named file or directory in cwd turned into a template path (exit 2, E100 Runtime error). And case 19's empty-baseline guard no longer lets five dependent comparisons print vacuous passes — while fixing it the guard turned out to be unreachable, since under pipefail an alert-free report aborted the script at the assignment. Refs: #85 ADR: 0014
This commit is contained in:
@@ -9,8 +9,10 @@ set -euo pipefail
|
||||
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
||||
# value keeps exactly the line breaks the source has, and vale matches it fine
|
||||
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
||||
# This script flattens an affected description to one physical line in a scratch
|
||||
# copy (padding with blank lines so every other line number is unchanged), then
|
||||
# This script flattens an affected description to a one-line scalar in a scratch
|
||||
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
|
||||
# `|-` literal block with a single content line, which vale matches just as well
|
||||
# (padding with blank lines so every other line number is unchanged), then
|
||||
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
||||
# `vale` directly: same args, same exit code, bar the two documented divergences
|
||||
# below.
|
||||
@@ -61,6 +63,20 @@ vale_args=()
|
||||
path_args=()
|
||||
pending_flag=""
|
||||
config_given=false
|
||||
|
||||
# `--output` takes either one of vale's built-in style names or a template file
|
||||
# path. Only the file form needs absolutizing, and the built-in names have to be
|
||||
# excluded by name *before* the existence test below: a file or directory
|
||||
# literally called `line` in the caller's cwd would otherwise rewrite the
|
||||
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
|
||||
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
|
||||
# names — it is always a path — so the check is keyed on the flag too.
|
||||
is_builtin_output() {
|
||||
case "$2" in
|
||||
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
|
||||
*) false ;;
|
||||
esac
|
||||
}
|
||||
for arg in "$@"; do
|
||||
if [[ -n "$pending_flag" ]]; then
|
||||
# Value of a separated two-argv flag. It is never a lint target, however
|
||||
@@ -76,11 +92,12 @@ for arg in "$@"; do
|
||||
fi
|
||||
;;
|
||||
--output|--path)
|
||||
# `--output` is either a built-in style name (`line`, `JSON`) or a
|
||||
# template file; only the file form needs resolving. `--path` is
|
||||
# likewise a path. Anything that names nothing is passed through and
|
||||
# See `is_builtin_output` above for why the built-in `--output` names
|
||||
# are excluded first. Anything that names nothing is passed through and
|
||||
# left for vale to interpret.
|
||||
if [[ "$arg" != /* && -e "$arg" ]]; then
|
||||
if is_builtin_output "$pending_flag" "$arg"; then
|
||||
vale_args+=("$arg")
|
||||
elif [[ "$arg" != /* && -e "$arg" ]]; then
|
||||
vale_args+=("$cwd/$arg")
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
@@ -114,7 +131,9 @@ for arg in "$@"; do
|
||||
# other path-valued flags.
|
||||
--output=*|--path=*)
|
||||
flag_val="${arg#*=}"
|
||||
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||
if is_builtin_output "${arg%%=*}" "$flag_val"; then
|
||||
vale_args+=("$arg")
|
||||
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
@@ -286,13 +305,14 @@ def continuation_lines(rest):
|
||||
|
||||
|
||||
def emit(value):
|
||||
"""Render `value` as a one-line YAML scalar whose source text spells the
|
||||
value out verbatim. Vale locates the description by matching the parsed
|
||||
value back against the source, so a scalar carrying any escape — `''` in a
|
||||
"""Render `value` as a YAML scalar whose source text spells the value out
|
||||
verbatim. Vale locates the description by matching the parsed value back
|
||||
against the source, so a scalar carrying any escape — `''` in a
|
||||
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
||||
whole `text.frontmatter.description` scope vanish, the same failure this
|
||||
script exists to work around. Verbatim forms only, therefore, tried in
|
||||
descending order of fidelity."""
|
||||
descending order of fidelity. The first three occupy one physical line; the
|
||||
`|-` fallback occupies two, which the caller accounts for when padding."""
|
||||
if (value
|
||||
and value[0] not in PLAIN_UNSAFE_FIRST
|
||||
and ': ' not in value
|
||||
@@ -304,11 +324,14 @@ def emit(value):
|
||||
if '"' not in value and '\\' not in value:
|
||||
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
||||
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
||||
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting
|
||||
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
|
||||
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
|
||||
# never written back to the real file.
|
||||
return "'" + value.replace("'", '’') + "'"
|
||||
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
|
||||
# literal block can — a block scalar's body has no escape syntax at all, so
|
||||
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
|
||||
# the description scope against it (the header above says the same of the
|
||||
# `|` blocks this script deliberately leaves alone; verified against vale
|
||||
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
|
||||
# value is exactly `value` with no trailing newline.
|
||||
return '|-\n ' + value
|
||||
|
||||
|
||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||
@@ -392,11 +415,21 @@ if header_m:
|
||||
newline = fm.find('\n', value_end)
|
||||
span_end = len(fm) if newline == -1 else newline + 1
|
||||
trailer = fm[value_end:span_end].rstrip('\n')
|
||||
# One line replaces the span, so the blank-line pad is one short of the
|
||||
# newline count it displaced — every later line number is unchanged.
|
||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
|
||||
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
|
||||
+ '\n' + pad + fm[span_end:])
|
||||
scalar = emit(flat)
|
||||
# A trailing comment carried across from the original line stays on the
|
||||
# `description:` line itself: after a block scalar's `|-` header it is
|
||||
# still a comment, but inside the block body it would become part of the
|
||||
# value.
|
||||
head, newline_sep, block_body = scalar.partition('\n')
|
||||
# The replacement displaces the whole span, so the blank-line pad makes
|
||||
# up the difference between the lines it displaced and the lines it
|
||||
# occupies — every later line number is unchanged. That is one line for
|
||||
# the three inline forms and two for the `|-` block; the span itself is
|
||||
# at least two lines here (`value_lines >= 2` is a precondition), so the
|
||||
# pad count never goes negative.
|
||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
|
||||
new_fm = (fm[:head_start] + 'description: ' + head + trailer
|
||||
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
|
||||
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
||||
+ content[fm_match.end():])
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ set -euo pipefail
|
||||
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
|
||||
# value keeps exactly the line breaks the source has, and vale matches it fine
|
||||
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
|
||||
# This script flattens an affected description to one physical line in a scratch
|
||||
# copy (padding with blank lines so every other line number is unchanged), then
|
||||
# This script flattens an affected description to a one-line scalar in a scratch
|
||||
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
|
||||
# `|-` literal block with a single content line, which vale matches just as well
|
||||
# (padding with blank lines so every other line number is unchanged), then
|
||||
# runs the real `vale` binary against the copies. Drop-in replacement for calling
|
||||
# `vale` directly: same args, same exit code, bar the two documented divergences
|
||||
# below.
|
||||
@@ -61,6 +63,20 @@ vale_args=()
|
||||
path_args=()
|
||||
pending_flag=""
|
||||
config_given=false
|
||||
|
||||
# `--output` takes either one of vale's built-in style names or a template file
|
||||
# path. Only the file form needs absolutizing, and the built-in names have to be
|
||||
# excluded by name *before* the existence test below: a file or directory
|
||||
# literally called `line` in the caller's cwd would otherwise rewrite the
|
||||
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
|
||||
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
|
||||
# names — it is always a path — so the check is keyed on the flag too.
|
||||
is_builtin_output() {
|
||||
case "$2" in
|
||||
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
|
||||
*) false ;;
|
||||
esac
|
||||
}
|
||||
for arg in "$@"; do
|
||||
if [[ -n "$pending_flag" ]]; then
|
||||
# Value of a separated two-argv flag. It is never a lint target, however
|
||||
@@ -76,11 +92,12 @@ for arg in "$@"; do
|
||||
fi
|
||||
;;
|
||||
--output|--path)
|
||||
# `--output` is either a built-in style name (`line`, `JSON`) or a
|
||||
# template file; only the file form needs resolving. `--path` is
|
||||
# likewise a path. Anything that names nothing is passed through and
|
||||
# See `is_builtin_output` above for why the built-in `--output` names
|
||||
# are excluded first. Anything that names nothing is passed through and
|
||||
# left for vale to interpret.
|
||||
if [[ "$arg" != /* && -e "$arg" ]]; then
|
||||
if is_builtin_output "$pending_flag" "$arg"; then
|
||||
vale_args+=("$arg")
|
||||
elif [[ "$arg" != /* && -e "$arg" ]]; then
|
||||
vale_args+=("$cwd/$arg")
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
@@ -114,7 +131,9 @@ for arg in "$@"; do
|
||||
# other path-valued flags.
|
||||
--output=*|--path=*)
|
||||
flag_val="${arg#*=}"
|
||||
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||
if is_builtin_output "${arg%%=*}" "$flag_val"; then
|
||||
vale_args+=("$arg")
|
||||
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
|
||||
vale_args+=("${arg%%=*}=$cwd/$flag_val")
|
||||
else
|
||||
vale_args+=("$arg")
|
||||
@@ -286,13 +305,14 @@ def continuation_lines(rest):
|
||||
|
||||
|
||||
def emit(value):
|
||||
"""Render `value` as a one-line YAML scalar whose source text spells the
|
||||
value out verbatim. Vale locates the description by matching the parsed
|
||||
value back against the source, so a scalar carrying any escape — `''` in a
|
||||
"""Render `value` as a YAML scalar whose source text spells the value out
|
||||
verbatim. Vale locates the description by matching the parsed value back
|
||||
against the source, so a scalar carrying any escape — `''` in a
|
||||
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
|
||||
whole `text.frontmatter.description` scope vanish, the same failure this
|
||||
script exists to work around. Verbatim forms only, therefore, tried in
|
||||
descending order of fidelity."""
|
||||
descending order of fidelity. The first three occupy one physical line; the
|
||||
`|-` fallback occupies two, which the caller accounts for when padding."""
|
||||
if (value
|
||||
and value[0] not in PLAIN_UNSAFE_FIRST
|
||||
and ': ' not in value
|
||||
@@ -304,11 +324,14 @@ def emit(value):
|
||||
if '"' not in value and '\\' not in value:
|
||||
return '"' + value + '"' # double-quoted: only `"`/`\` would
|
||||
# Last resort: the value needs quoting AND holds an apostrophe AND a double
|
||||
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting
|
||||
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
|
||||
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
|
||||
# never written back to the real file.
|
||||
return "'" + value.replace("'", '’') + "'"
|
||||
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
|
||||
# literal block can — a block scalar's body has no escape syntax at all, so
|
||||
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
|
||||
# the description scope against it (the header above says the same of the
|
||||
# `|` blocks this script deliberately leaves alone; verified against vale
|
||||
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
|
||||
# value is exactly `value` with no trailing newline.
|
||||
return '|-\n ' + value
|
||||
|
||||
|
||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||
@@ -392,11 +415,21 @@ if header_m:
|
||||
newline = fm.find('\n', value_end)
|
||||
span_end = len(fm) if newline == -1 else newline + 1
|
||||
trailer = fm[value_end:span_end].rstrip('\n')
|
||||
# One line replaces the span, so the blank-line pad is one short of the
|
||||
# newline count it displaced — every later line number is unchanged.
|
||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
|
||||
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
|
||||
+ '\n' + pad + fm[span_end:])
|
||||
scalar = emit(flat)
|
||||
# A trailing comment carried across from the original line stays on the
|
||||
# `description:` line itself: after a block scalar's `|-` header it is
|
||||
# still a comment, but inside the block body it would become part of the
|
||||
# value.
|
||||
head, newline_sep, block_body = scalar.partition('\n')
|
||||
# The replacement displaces the whole span, so the blank-line pad makes
|
||||
# up the difference between the lines it displaced and the lines it
|
||||
# occupies — every later line number is unchanged. That is one line for
|
||||
# the three inline forms and two for the `|-` block; the span itself is
|
||||
# at least two lines here (`value_lines >= 2` is a precondition), so the
|
||||
# pad count never goes negative.
|
||||
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
|
||||
new_fm = (fm[:head_start] + 'description: ' + head + trailer
|
||||
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
|
||||
content = (fm_match.group(1) + new_fm + fm_match.group(3)
|
||||
+ content[fm_match.end():])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user