singe/util/frenchSpaces.py

77 lines
2.6 KiB
Python

# Puts the no-break spaces French typography wants into the French books: before ; : ! ? and
# inside guillemets. A translator types ordinary spaces; this makes them U+00A0 in prose only,
# leaving listings, inline code, attribute and block attribute lines, comments, and AsciiDoc's own
# colons (term::, NOTE:, macros) alone. Run it after editing docs/fr, then util/checkTranslations.rb.
#
# Usage: python3 util/frenchSpaces.py [file...] every .adoc under docs/fr, or the files named
import glob
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
NBSP = "\u00a0"
FENCE = re.compile(r"^(-{4,}|\.{4,}|\+{4,}|/{4,})$")
BEFORE = re.compile("(?<=\\S) ([;:!?\u00bb])(?=\\s|$|[\u00bb)\\]*_,.])")
AFTER = re.compile("\u00ab ")
CODE = re.compile(r"`[^`\n]*`|\b[a-z]+:\S*\[")
LINK = re.compile(r"https?://[^\s\[\]<>]+")
HOLD = "\x00%d\x00"
LINKED = "\x01%d\x01"
def fixProse(line):
# Inline code, web addresses, and macros are set aside behind placeholders while the spaces are
# fixed, so a space after one (`DEBUG_SHAPES` :) is still seen as following a word.
kept = []
def keep(match):
kept.append(match.group(0))
return HOLD % (len(kept) - 1)
def keepLink(match):
kept.append(match.group(0))
return LINKED % (len(kept) - 1)
# A web address ends at the first space, and Asciidoctor takes a no-break space as part of it,
# so the space after one stays ordinary.
fixed = fixStretch(CODE.sub(keep, LINK.sub(keepLink, line)))
fixed = re.sub(r"(\x01\d+\x01)" + NBSP, r"\1 ", fixed)
return re.sub(r"[\x00\x01](\d+)[\x00\x01]", lambda m: kept[int(m.group(1))], fixed)
def fixStretch(text):
# A colon followed by another is AsciiDoc's (term::), never French punctuation.
text = BEFORE.sub(lambda m: m.group(0) if (m.group(1) == ":" and text[m.end():m.end() + 1] == ":") else NBSP + m.group(1), text)
return AFTER.sub("\u00ab" + NBSP, text)
def fixFile(path):
lines = open(path, encoding="utf-8").read().split("\n")
fence = None
changed = 0
for index, line in enumerate(lines):
if FENCE.match(line):
fence = None if fence == line else (fence or line)
continue
if fence or line.startswith((":", "[", "//", "include::", "image::")) or line.startswith("|==="):
continue
fixed = fixProse(line)
if fixed != line:
lines[index] = fixed
changed += 1
if changed:
open(path, "w", encoding="utf-8").write("\n".join(lines))
return changed
def main():
paths = sys.argv[1:] or sorted(glob.glob(os.path.join(REPO, "docs", "fr", "**", "*.adoc"), recursive=True))
for path in paths:
changed = fixFile(path)
if changed:
print("%s: %d lines" % (os.path.relpath(path, REPO), changed))
main()