# 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()