77 lines
2.6 KiB
Python
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()
|