polytypo 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -1
  3. data/lib/polytypo/data/VERSION +1 -1
  4. data/lib/polytypo/data/fixtures/cs.json +161 -0
  5. data/lib/polytypo/data/fixtures/de-CH.json +1 -1
  6. data/lib/polytypo/data/fixtures/de-DE.json +195 -6
  7. data/lib/polytypo/data/fixtures/el.json +1 -1
  8. data/lib/polytypo/data/fixtures/en-GB.json +12 -1
  9. data/lib/polytypo/data/fixtures/en-US.json +648 -1
  10. data/lib/polytypo/data/fixtures/es.json +193 -0
  11. data/lib/polytypo/data/fixtures/fi.json +1 -1
  12. data/lib/polytypo/data/fixtures/fr-CA.json +25 -1
  13. data/lib/polytypo/data/fixtures/fr.json +176 -1
  14. data/lib/polytypo/data/fixtures/it.json +161 -0
  15. data/lib/polytypo/data/fixtures/locale-resolution.json +76 -4
  16. data/lib/polytypo/data/fixtures/nl.json +121 -0
  17. data/lib/polytypo/data/fixtures/pl.json +137 -0
  18. data/lib/polytypo/data/fixtures/pt-BR.json +156 -0
  19. data/lib/polytypo/data/fixtures/pt-PT.json +156 -0
  20. data/lib/polytypo/data/fixtures/ru.json +23 -1
  21. data/lib/polytypo/data/fixtures/sv.json +1 -1
  22. data/lib/polytypo/data/fixtures/uk.json +153 -0
  23. data/lib/polytypo/data/locales/cs.json +90 -0
  24. data/lib/polytypo/data/locales/de-DE.json +7 -2
  25. data/lib/polytypo/data/locales/en-US.json +3 -3
  26. data/lib/polytypo/data/locales/es.json +111 -0
  27. data/lib/polytypo/data/locales/fr-CA.json +7 -1
  28. data/lib/polytypo/data/locales/fr.json +7 -1
  29. data/lib/polytypo/data/locales/it.json +95 -0
  30. data/lib/polytypo/data/locales/nl.json +84 -0
  31. data/lib/polytypo/data/locales/pl.json +96 -0
  32. data/lib/polytypo/data/locales/pt-BR.json +82 -0
  33. data/lib/polytypo/data/locales/pt-PT.json +84 -0
  34. data/lib/polytypo/data/locales/registry.json +23 -3
  35. data/lib/polytypo/data/locales/ru.json +2 -2
  36. data/lib/polytypo/data/locales/uk.json +130 -0
  37. data/lib/polytypo/data/rules/analyze.md +157 -0
  38. data/lib/polytypo/data/rules/apostrophe.md +432 -0
  39. data/lib/polytypo/data/rules/dashes.md +128 -37
  40. data/lib/polytypo/data/rules/ellipsis.md +271 -0
  41. data/lib/polytypo/data/rules/hyphen.md +353 -0
  42. data/lib/polytypo/data/rules/locale-resolution.md +239 -0
  43. data/lib/polytypo/data/rules/modes.md +1281 -0
  44. data/lib/polytypo/data/rules/nbsp.md +1157 -0
  45. data/lib/polytypo/data/rules/order.json +11 -11
  46. data/lib/polytypo/data/rules/pipeline-idempotency.md +605 -0
  47. data/lib/polytypo/data/rules/quotes.md +1324 -0
  48. data/lib/polytypo/data/rules/ranges.md +489 -0
  49. data/lib/polytypo/data/rules/spaces.md +649 -0
  50. data/lib/polytypo/data/rules/symbols.md +540 -0
  51. data/lib/polytypo/data/schema/fixtures.schema.json +18 -3
  52. data/lib/polytypo/engine/origin.rb +75 -0
  53. data/lib/polytypo/engine/pipeline.rb +72 -1
  54. data/lib/polytypo/engine/rules/dash_shared.rb +85 -3
  55. data/lib/polytypo/engine/rules/dashes.rb +4 -1
  56. data/lib/polytypo/engine/rules/nbsp.rb +43 -7
  57. data/lib/polytypo/engine/rules/ranges.rb +24 -20
  58. data/lib/polytypo/errors.rb +3 -0
  59. data/lib/polytypo/modes/runner.rb +17 -0
  60. data/lib/polytypo/modes/spans.rb +30 -2
  61. data/lib/polytypo/modes/yaml.rb +312 -0
  62. data/lib/polytypo/version.rb +1 -1
  63. data/lib/polytypo.rb +126 -15
  64. metadata +31 -1
@@ -0,0 +1,84 @@
1
+ {
2
+ "locale": "pt-PT",
3
+ "name": "Portuguese (Portugal)",
4
+ "quotes": {
5
+ "primary": {
6
+ "open": "«",
7
+ "close": "»",
8
+ "innerSpace": "none"
9
+ },
10
+ "secondary": {
11
+ "open": "“",
12
+ "close": "”",
13
+ "innerSpace": "none"
14
+ },
15
+ "elisionIdioms": []
16
+ },
17
+ "dash": {
18
+ "parenthetical": "em-spaced",
19
+ "range": "none"
20
+ },
21
+ "ellipsis": {
22
+ "abbreviatedAfterTerminal": false
23
+ },
24
+ "hyphen": {
25
+ "prefixes": [],
26
+ "suffixes": [],
27
+ "compounds": []
28
+ },
29
+ "nbsp": {
30
+ "beforePunctuation": [],
31
+ "narrowBeforePunctuation": [],
32
+ "afterShortWords": [],
33
+ "abbreviations": ["p. ex."],
34
+ "beforeUnits": ["%", "‰", "°C", "km", "cm", "mm", "kg", "kW", "kWh", "Hz"],
35
+ "beforeNumber": ["p."],
36
+ "beforeWord": [],
37
+ "afterSymbols": [],
38
+ "initialBinding": "none"
39
+ },
40
+ "sources": [
41
+ {
42
+ "rule": "quotes",
43
+ "cite": "Serviço das Publicações da União Europeia, Código de Redação Interinstitucional (edição PT, 2022, ISBN 978-92-78-42820-4), Quarta parte «Convenções próprias da língua portuguesa», ponto 10.4.10 «Aspas», N. B.: «O primeiro nível de aspas a utilizar corresponde às aspas angulares («»). Quando necessário, utilizam-se três níveis de aspas devidamente hierarquizadas: «…» — as aspas angulares, “…” — as aspas curvas duplas ou vírgulas dobradas, ‘…’ — as aspas curvas simples ou vírgulas simples»",
44
+ "url": "https://op.europa.eu/pt/publication-detail/-/publication/01ed788a-d266-11ec-a95f-01aa75ed71a1",
45
+ "note": "Primário U+00AB/U+00BB, secundário U+201C/U+201D. O terceiro nível descrito pela fonte não é exprimível em locale.schema.json, que só tem primary e secondary — limite do esquema, não lacuna da fonte, como já consta de el.json. innerSpace = \"none\": todos os exemplos do ponto 10.4.10 escrevem o texto colado às aspas e a secção nunca pede espaço interior. A Quarta parte é o capítulo específico da língua portuguesa; a Terceira parte do mesmo guia é deliberadamente NÃO invocada, porque o seu §6.4 declara que as suas regras «são o resultado de um acordo interinstitucional» e que «certas escolhas foram feitas em prol de uma convenção comum» — estilo de editor, não prova sobre o português, a mesma desqualificação que el.json regista. É POR ISTO que esta locale é pt-PT e não pt: o uso brasileiro diverge e tem ficheiro próprio (pt-BR), em vez de ser apanhado por um fallback que lhe daria aspas que não usa. Consultado em 18.9.2026."
46
+ },
47
+ {
48
+ "rule": "dashes",
49
+ "cite": "Código de Redação Interinstitucional (PT, 2022), Quarta parte, ponto 10.4.8 «Travessão»: «O travessão (traço horizontal maior que o hífen) emprega-se: […] d) No lugar de parênteses: As condições — ordenado e subvenções — eram boas»",
50
+ "url": "https://op.europa.eu/pt/publication-detail/-/publication/01ed788a-d266-11ec-a95f-01aa75ed71a1",
51
+ "note": "Justifica dash.parenthetical = \"em-spaced\". O papel (substitui parênteses) e o espaçamento (um espaço ordinário de cada lado de CADA travessão) estão no exemplo impresso; ao contrário do grego, do espanhol e do italiano, o português não usa o padrão «espaços fora do par, colado por dentro», pelo que o enum exprime a convenção sem forçar nada. A LARGURA foi verificada extraindo a camada de texto do PDF, não julgada a olho sobre a página rasterizada: o carácter é U+2014. Esta é a segunda divergência regional do português — o Manual de Redação da Presidência da República (Brasil) define o travessão como «um hífen prolongado» e o seu texto extraído dá U+2013 —, e é a segunda razão para pt-PT e pt-BR serem ficheiros distintos. Consultado em 18.9.2026."
52
+ },
53
+ {
54
+ "rule": "ranges",
55
+ "cite": "Código de Redação Interinstitucional (PT, 2022), ponto 10.4.11: «Quando se trata de dois anos completos ou de um período de vários anos usa-se o hífen: o programa para 1996-1997; o período de 1993-1996»; ponto 10.9.1: «Para qualquer período que decorra entre dois anos consecutivos, estes são separados por um hífen, se o período abrange a totalidade dos dois anos (1990-1991), e por uma barra, em caso contrário (ano letivo de 1990/1991)»",
56
+ "url": "https://op.europa.eu/pt/publication-detail/-/publication/01ed788a-d266-11ec-a95f-01aa75ed71a1",
57
+ "note": "Justifica dash.range = \"none\" pela positiva. A regra é enunciada duas vezes, em dois capítulos da parte específica da língua, e as duas vezes nomeia o hífen; a barra fica reservada para o período que não abrange os dois anos completos. «none» é o único valor do enum que respeita a fonte, porque `ranges` não emite nada nesse modo (ranges.md §2) e o U+002D escrito pelo autor sobrevive — substituí-lo por U+2013 contrariaria a citação. Consultado em 18.9.2026."
58
+ },
59
+ {
60
+ "rule": "ellipsis",
61
+ "cite": "Código de Redação Interinstitucional (PT, 2022), ponto 10.4.7 «Reticências», com o exemplo impresso «É o dianho!… É o dianho!… E, olha lá, porque não vais ter com o padre?…» (Aquilino Ribeiro)",
62
+ "url": "https://op.europa.eu/pt/publication-detail/-/publication/01ed788a-d266-11ec-a95f-01aa75ed71a1",
63
+ "note": "Justifica abbreviatedAfterTerminal = false. O passo citado escreve as reticências completas imediatamente a seguir a «!» e a «?» — «!…» e «?…» —, e nenhuma fonte consultada prevê a forma abreviada de dois pontos que o russo usa, pelo que esse ramo de ellipsis.md fica desligado para o português. Nenhuma fonte se pronuncia sobre U+2026 contra três U+002E: essa escolha é do motor e é igual em todas as locales. Consultado em 18.9.2026."
64
+ },
65
+ {
66
+ "rule": "hyphen",
67
+ "cite": "Acordo Ortográfico da Língua Portuguesa (1990), Base XX «Da divisão silábica»: «Na translineação de uma palavra composta ou de uma combinação de palavras em que há um hífen, ou mais, se a partição coincide com o final de um dos elementos ou membros, deve, por clareza gráfica, repetir-se o hífen no início da linha imediata: ex- -alferes»",
68
+ "url": "http://www.portaldalinguaportuguesa.org/?action=acordo&version=1990",
69
+ "note": "Justifica as três listas vazias, e justifica-as pela positiva, não por ausência de fonte. O português não só permite como PRESCREVE a quebra de linha sobre o hífen de um composto, repetindo-o na linha seguinte: uma forma que é normativamente quebrada nesse ponto não pode ser uma forma cujo hífen tenha de resistir à quebra, que é a única coisa que esta regra consome. A regra é, portanto, um no-op total demonstrável para o português (hyphen.md §2), e qualquer runtime que ligue compostos portugueses com U+2011 está errado. Corroborado pelo Código de Redação Interinstitucional (PT, 2022), ponto 10.2 e ponto 10.4.12 («segunda-/-feira», «salmão-do-/-atlântico»), e pelo Manual de Redação da Presidência da República (Brasil, 2.ª ed., 2002), §9.1.3.1 («decreto-/-lei», «far-/-se-á»). É o campo em que Portugal, o Brasil e o próprio Acordo dizem explicitamente a mesma coisa. Consultado em 18.9.2026."
70
+ },
71
+ {
72
+ "rule": "nbsp",
73
+ "cite": "Código de Redação Interinstitucional (PT, 2022), anexo A3 «Abreviaturas e símbolos», pontos 2 e 3, com as Observações «Todos os números são acompanhados dos respetivos símbolos: de 4 m em 4 m; de 1 kg, 6 kg a 15 kg», e Quarta parte, ponto 10.9.1: «As percentagens, pesos e medidas escrevem-se numericamente: 7 % do volume de negócios», «a temperatura atingiu hoje os 39 °C»; BIPM, The International System of Units (SI), 9.ª ed., resumo conciso: «A single space is always left between the number and the unit»",
74
+ "url": "https://www.bipm.org/documents/20126/41483022/SI-Brochure-9-concise-EN.pdf",
75
+ "note": "Justifica nbsp.beforeUnits. A locale atesta a PERTENÇA — que estes símbolos são unidades e que se escrevem depois do número com um espaço; o mecanismo (converter esse espaço em U+00A0, nunca o inserir) é da regra e está fixado em nbsp.md §2.1. REPARO CITADO, mais importante do que qualquer inclusão: o ponto 10.9.1 exclui «h» por nome — «horas (o símbolo «h» escreve-se sempre sem ponto, sem espaços): eram as 18h30» —, pelo que «h» NÃO entra na lista e há um caso de conformidade a fixá-lo. Os símbolos de uma só letra (m, g, l, t, s, A, W, V, K) ficam igualmente de fora, embora atestados, pela mesma razão que em fr: não são desambiguáveis sem contexto. Note-se que o português escreve «7 %» com espaço, ao contrário do grego. Consultado em 18.9.2026."
76
+ },
77
+ {
78
+ "rule": "nbsp",
79
+ "cite": "Código de Redação Interinstitucional (PT, 2022), anexo A3, ponto 4 «Obras»: «p. ex. por exemplo»; «e. g. exempli gratia (por exemplo). Utilizar de preferência p. ex.»; Quarta parte, ponto 10.9.1: «paginação corrente, parágrafos, artigos: artigo 2.º, terceiro parágrafo, alínea b), p. 24»; nota (3) do ponto 6.4: «Para o «º» ordinal (1.º, 2.º…) ou em «n.º», utilizar a terminação «o» após o ponto e em posição superior à linha [não utilizar o sinal «º» do teclado Azerty nem a sequência Alt 0176 (símbolo do grau «°»)]»",
80
+ "url": "https://op.europa.eu/pt/publication-detail/-/publication/01ed788a-d266-11ec-a95f-01aa75ed71a1",
81
+ "note": "Justifica as listas vazias e initialBinding = \"none\", e diz com exatidão em que se apoiam: ao contrário do guia grego, a parte portuguesa NÃO contém nenhuma frase que negue pelo nome o uso francês do espaço antes da pontuação dupla. A posição é «nenhuma fonte o exige», e não «uma fonte o proíbe»; o que se observa é que nenhum exemplo do capítulo o escreve, e isso é uma observação, assinalada como tal. beforeNumber contém apenas «p.»: o ponto 10.9.1 atesta a ligação («p. 24», «Lei n.º 123»), mas «n.º» NÃO pode ser listado, porque a nota (3) do ponto 6.4 do mesmo guia prescreve um «o» sobrescrito e RECUSA expressamente tanto U+00BA como U+00B0 — listar «n.º» seria citar uma fonte que proíbe esse mesmo carácter. A decisão do operador de 18.9.2026 sobre o «n°» francês cobre uma fonte SILENCIOSA e não se estende a uma que fala e recusa. beforeWord fica vazio: o anexo A3 enumera «Sr.», «Dr.», «Prof.», mas limita-se a expandi-los e nenhuma fonte os mostra ligados ao nome seguinte. GAP REGISTADO, não resolvido: o ponto 10.9.1 exige espaço protegido DENTRO dos grupos de algarismos («um total de 12 345 euros»; «este espaço é protegido»), e locale.schema.json não tem campo para isso. Consultado em 18.9.2026."
82
+ }
83
+ ]
84
+ }
@@ -1,9 +1,29 @@
1
1
  {
2
- "spec": "1.2.0",
2
+ "spec": "1.3.1",
3
3
  "$comment": "Locale resolution input. Algorithm is specified in spec/rules/locale-resolution.md and is identical in every runtime — never delegate it to a platform locale-negotiation library. \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
4
- "locales": ["en-US", "en-GB", "de-DE", "de-CH", "fr", "fr-CA", "ru", "fi", "sv", "el"],
4
+ "locales": [
5
+ "en-US",
6
+ "en-GB",
7
+ "de-DE",
8
+ "de-CH",
9
+ "fr",
10
+ "fr-CA",
11
+ "ru",
12
+ "fi",
13
+ "sv",
14
+ "el",
15
+ "es",
16
+ "it",
17
+ "pt-PT",
18
+ "pt-BR",
19
+ "nl",
20
+ "pl",
21
+ "uk",
22
+ "cs"
23
+ ],
5
24
  "aliases": {
6
25
  "en": "en-US",
7
- "de": "de-DE"
26
+ "de": "de-DE",
27
+ "pt": "pt-PT"
8
28
  }
9
29
  }
@@ -94,13 +94,13 @@
94
94
  "rule": "nbsp",
95
95
  "cite": "Мильчин А. Э., Чельцова Л. К. «Справочник издателя и автора»: неразрывный пробел ставится между инициалами и между инициалами и фамилией («А. С. Пушкин»), после однобуквенных предлогов и союзов, между знаками № и § и относящимися к ним числами, между числом и относящейся к нему единицей измерения или счётным словом («1981 г.», «5 млн»), а также внутри сокращений «и т. д.», «и т. п.», «т. е.»",
96
96
  "url": "https://orfogrammka.ru/%D1%82%D0%B8%D0%BF%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0/%D0%BF%D1%80%D0%BE%D0%B1%D0%B5%D0%BB_%D0%B8_%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D1%8B/",
97
- "note": "Список afterShortWords сознательно ограничен однобуквенными предлогами и союзами (а, в, и, к, о, с, у) и однозначными двухбуквенными предлогами (во, до, за, из, ко, на, об, от, по, со). Частицы и союзы «не», «ни», «но», «же», «ли», «бы» не включены: правило для них — рекомендация вёрстки, а не норма, и связывание их даёт заметный риск ложных срабатываний. Префиксные сокращения «г. Москва», «ул. Ленина» (docs/PLAN.md §7) выразить нельзя: afterSymbols по описанию схемы связывает символ со следующим ЧИСЛОМ, а не со словом; «г.» здесь помещено в beforeUnits в значении «год» после числа («2020 г.»)."
97
+ "note": "Список afterShortWords сознательно ограничен однобуквенными предлогами и союзами (а, в, и, к, о, с, у) и однозначными двухбуквенными предлогами (во, до, за, из, ко, на, об, от, по, со). Частицы и союзы «не», «ни», «но», «же», «ли», «бы» не включены: правило для них — рекомендация вёрстки, а не норма, и связывание их даёт заметный риск ложных срабатываний. Префиксные сокращения из строки «ru» в docs/PLAN.md §7 («г.», «ул.») с появлением beforeWord и N10 стали выразимы: «ул.» и «пл.» перечислены там, обоснование — в следующей записи. «г.» остаётся невыразимым, но не из-за формы поля: здесь оно помещено в beforeUnits в значении «год» после числа («2020 г.»), а в значении «город» связывается вправо, и литеральный список эти два смысла не различает."
98
98
  },
99
99
  {
100
100
  "rule": "nbsp",
101
101
  "cite": "«Технические правила набора», раздел «Общие требования к набору», п. 5: «Отбиваются слова от имен собственных, к которым они относятся (ул. Советская)»; раздел «Переносы», п. 4: «Не должны быть отделены при переносе из одной строки в другую: а) фамилии от инициалов или один инициал от другого; б) сокращенные слова имен собственных, к которым они относятся, например: тов. Сергеева, г. Гомель, пл. Ленина; … г) арабские или римские цифры от их сокращенных или полных наименований, например: 2010 г., 800 руб., 50 куб. см, XX век; д) знаки и обозначения (№, §, % и т. п.) от следующих за ними цифр»",
102
102
  "url": "https://old.gsu.by/pages/izdat/2023/tehnicheskie_pravila_nabora.pdf",
103
- "note": "Текст сверен по самому PDF; это издательский свод технических правил набора (Гомельский госуниверситет), то есть опубликованное изложение отраслевых правил (Мильчин, ОСТ 29.115-88), а не первоисточник: главу «Технические правила набора и вёрстки» у Мильчина проверить онлайн не удалось (доступное издание 1998 г. содержит только части 1—3 без этой главы). Пункт 4б — единственное найденное нормативное основание для beforeWord. «г.» в этот список сознательно НЕ включено: «г.» нормативно связывается и влево как «год» (п. 4г, «2010 г.», уже отражено в beforeUnits), и вправо как «город» (п. 4б, «г. Гомель»), а литеральный список без контекста эти два случая не различает — во фразе «в 1147 г. Москва была основана» правило beforeWord склеило бы «г.» с «Москва» через границу оборота. Различить их можно, только посмотрев влево на цифру и вправо на топоним, то есть по контексту, чего декларативный список не выражает. По той же причине не включены «гг.», «в.», «вв.». Оставлены «ул.» и «пл.» — однозначные топонимические сокращения, и обе формы процитированы дословно: «пл. Ленина» в «Переносах» п. 4б, «ул. Советская» в «Общих требованиях к набору» п. 5. «д.», «стр.», «корп.», «кв.» относятся к следующему ЧИСЛУ, а не к слову, поэтому в beforeWord им не место. beforeNumber пуст, и это подтверждённое отсутствие правила, а не ненайденная цитата: перечень «Переносов» п. 4 закрытый и исчерпывающий, он содержит обратное направление (п. 4г, «2010 г.») и знаки (п. 4д, «№ 75»), но конструкции «сокращение + следующее число» в нём нет. Мильчин, «Справочник издателя и автора», § 9.1.1 («Употребление в ссылках сокращений слов и условных сокращений»: «Книговедческие термины при цифровых номерах или литерах рекомендуется для экономии места сокращать») нормирует, КАКОЕ сокращение писать перед числом, но о пробеле не говорит ничего. Страница «Орфограммки» с примером «гл. IV» отвергнута: она ссылается только на Википедию и Хабр."
103
+ "note": "Текст сверен по самому PDF; это издательский свод технических правил набора (Гомельский госуниверситет), то есть опубликованное изложение отраслевых правил (Мильчин, ОСТ 29.115-88), а не первоисточник: главу «Технические правила набора и вёрстки» у Мильчина проверить онлайн не удалось (доступное издание 1998 г. содержит только части 1—3 без этой главы). Пункт 4б — единственное найденное нормативное основание для beforeWord. «г.» в этот список сознательно НЕ включено: «г.» нормативно связывается и влево как «год» (п. 4г, «2010 г.», уже отражено в beforeUnits), и вправо как «город» (п. 4б, «г. Гомель»), а литеральный список без контекста эти два случая не различает — во фразе «в 1147 г. Москва была основана» правило beforeWord склеило бы «г.» с «Москва» через границу оборота. Различить их можно, только посмотрев влево на цифру и вправо на топоним, то есть по контексту, чего декларативный список не выражает. По той же причине не включены «гг.», «в.», «вв.». Оставлены «ул.» и «пл.» — однозначные топонимические сокращения, и обе формы процитированы дословно: «пл. Ленина» в «Переносах» п. 4б, «ул. Советская» в «Общих требованиях к набору» п. 5. «д.», «стр.», «корп.», «кв.» относятся к следующему ЧИСЛУ, а не к слову, поэтому в beforeWord им не место. Для «стр.» есть и более сильное основание, проверенное по изданию 1998 г. при разборе issue #30: § 4.4.4 («Сокращения при внутритекстовых ссылках и сопоставлениях») пишет дословно: «Ранее широко применявшееся сокращение стр. (страница) рекомендуется заменять сокращением с., поскольку оно закреплено ГОСТ 7.12—93, а две формы сокращения одного слова нарушают принцип единообразия». То есть источник не просто молчит о пробеле — он не рекомендует саму форму. Там же: «Все сокращения, кроме см. и ср., употребляются только в сочетании с цифрами или буквами», что подтверждает состав списка, но о самой отбивке по-прежнему не говорит. beforeNumber пуст, и это подтверждённое отсутствие правила, а не ненайденная цитата: перечень «Переносов» п. 4 закрытый и исчерпывающий, он содержит обратное направление (п. 4г, «2010 г.») и знаки (п. 4д, «№ 75»), но конструкции «сокращение + следующее число» в нём нет. Мильчин, «Справочник издателя и автора», § 9.1.1 («Употребление в ссылках сокращений слов и условных сокращений»: «Книговедческие термины при цифровых номерах или литерах рекомендуется для экономии места сокращать») нормирует, КАКОЕ сокращение писать перед числом, но о пробеле не говорит ничего. Страница «Орфограммки» с примером «гл. IV» отвергнута: она ссылается только на Википедию и Хабр."
104
104
  },
105
105
  {
106
106
  "rule": "hyphen",
@@ -0,0 +1,130 @@
1
+ {
2
+ "locale": "uk",
3
+ "name": "Ukrainian",
4
+ "quotes": {
5
+ "primary": {
6
+ "open": "«",
7
+ "close": "»",
8
+ "innerSpace": "none"
9
+ },
10
+ "secondary": {
11
+ "open": "“",
12
+ "close": "”",
13
+ "innerSpace": "none"
14
+ },
15
+ "elisionIdioms": []
16
+ },
17
+ "dash": {
18
+ "parenthetical": "em-spaced",
19
+ "range": "em-tight"
20
+ },
21
+ "ellipsis": {
22
+ "abbreviatedAfterTerminal": true
23
+ },
24
+ "hyphen": {
25
+ "prefixes": ["будь-", "казна-", "хтозна-", "бозна-"],
26
+ "suffixes": ["-бо", "-но", "-от", "-то", "-таки", "-будь", "-небудь"],
27
+ "compounds": [
28
+ "вид-во",
29
+ "гр-н",
30
+ "ін-т",
31
+ "р-н",
32
+ "ун-т",
33
+ "ф-ка",
34
+ "т-во",
35
+ "з-за",
36
+ "із-за",
37
+ "з-над",
38
+ "з-перед",
39
+ "з-під",
40
+ "із-під",
41
+ "з-поза",
42
+ "з-поміж",
43
+ "з-понад",
44
+ "з-попід",
45
+ "з-посеред",
46
+ "з-проміж"
47
+ ]
48
+ },
49
+ "nbsp": {
50
+ "beforePunctuation": [],
51
+ "narrowBeforePunctuation": [],
52
+ "afterShortWords": [],
53
+ "abbreviations": ["і т. д.", "і т. ін.", "та ін.", "куб. см"],
54
+ "beforeUnits": [
55
+ "%",
56
+ "га",
57
+ "год",
58
+ "дм",
59
+ "кБ",
60
+ "кВт",
61
+ "кг",
62
+ "км",
63
+ "мм",
64
+ "см",
65
+ "хв",
66
+ "Вт",
67
+ "км/год",
68
+ "м/с",
69
+ "млн",
70
+ "млрд",
71
+ "трлн",
72
+ "грн",
73
+ "р."
74
+ ],
75
+ "beforeNumber": [],
76
+ "beforeWord": ["акад.", "доц.", "проф."],
77
+ "afterSymbols": [],
78
+ "initialBinding": "chain"
79
+ },
80
+ "sources": [
81
+ {
82
+ "rule": "quotes",
83
+ "cite": "Український правопис (2019), схвалений Кабінетом Міністрів України (Постанова № 437 від 22.05.2019), § 164 «ЛАПКИ (« », “ ”, „ “, рідше „ ”)», п. 3: «У функції перших рекомендовано вживати кутові лапки, або «лапки-ялинки» («…»), у функції внутрішніх — «лапки-лапки» (“…” та ін.): «Це мій “Кобзар”», — сказав він»; там само: «На письмі (у рукописних текстах) «лапки-лапки» традиційно використовують у формі „…“»",
84
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
85
+ "note": "Зовнішні лапки — U+00AB і U+00BB, внутрішні — U+201C і U+201D. Код кожного знака встановлено за друкованим зображенням с. 247 офіційного видання, а не за текстовою конверсією: у прикладі § 164 п. 3 обидва внутрішні знаки підняті до верху рядка, відкривальний має форму 6, закривальний — форму 9. ЦЕ СПРОСТОВУЄ поширене припущення, що українська бере всередину „…“: заголовок § 164 справді дозволяє чотири пари, але саме п. 3 відносить „…“ до РУКОПИСНИХ текстів, а для друкованого рекомендує “…”. innerSpace = «none»: жодне джерело не вимагає відступу всередині лапок. Третій рівень вкладення в locale.schema.json невиразний. Звірено 18.09.2026."
86
+ },
87
+ {
88
+ "rule": "dashes",
89
+ "cite": "Український правопис (2019), § 161 «ТИРЕ (—)», I, п. 10—11 і Примітка 2: тире ставимо перед відокремленим зворотом або вставленою конструкцією — «Топольський — молодий чоловік, але — на думку пана посла — незвичайно талановитий і солідний» (О. Маковей)",
90
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
91
+ "note": "Обґрунтовує dash.parenthetical = «em-spaced». ДОВЖИНА встановлена прямо: заголовок параграфа — «ТИРЕ (—)», тобто U+2014, і цей самий знак стоїть у кожному прикладі §§ 161, 166, 167. ВІДБИВКА встановлена протиставленням, а не прозовим формулюванням: правопис ніде не пише «тире відбивається пробілами», але Примітка до п. 14 каже «тире ставимо без відступів між знаками» саме для випадку між цифрами, а «без відступів» має сенс лише як відхилення від відбитого за замовчуванням. Це єдиний запис цього файлу, що спирається на висновок із тексту джерела, і він позначений як такий свідомо. Звірено 18.09.2026."
92
+ },
93
+ {
94
+ "rule": "ranges",
95
+ "cite": "Український правопис (2019), § 161 «ТИРЕ (—)», I, п. 14, Примітка: «Між цифрами в таких випадках тире ставимо без відступів між знаками: у 2010—2018 роках; пам'ятки української мови XVI—XVIII ст.; на сторінках 1—10; у 1—4 томах, але, напр.: наприкінці XX — на початку XXI ст.»",
96
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
97
+ "note": "Обґрунтовує dash.range = «em-tight»: довге тире U+2014 без відбивки. Протиставлення в самій Примітці («на сторінках 1—10» без відступів, але «наприкінці XX — на початку XXI ст.» з відступами) показує, що безвідступна форма стосується рівно того, що робить правило ranges. Правило вимкнене за замовчуванням (spec 0.5.0). Звірено 18.09.2026."
98
+ },
99
+ {
100
+ "rule": "ellipsis",
101
+ "cite": "Український правопис (2019), § 162 «ТРИ КРАПКИ, АБО КРАПКИ (…)», Примітка: «у постпозиції — після знака питання і знака оклику — ставимо дві крапки: Стражники на людей стріляли, це відомо, а щоб селяни?.. (К. Гордієнко); Встає народ, гудуть мости, Рокочуть ріки ясноводі!.. (М. Рильський)»; § 166 «КОМБІНОВАНЕ ВЖИВАННЯ РОЗДІЛОВИХ ЗНАКІВ», п. 2, що перелічує допустимі поєднання як «…?; …!; ?..; !..»",
102
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
103
+ "note": "Обґрунтовує abbreviatedAfterTerminal = true. Українська — друга після російської локаль із цим значенням, і це перевірено окремо за двома параграфами, а не перенесено за аналогією зі спорідненої мови. Звірено 18.09.2026."
104
+ },
105
+ {
106
+ "rule": "hyphen",
107
+ "cite": "Український правопис (2019), § 64 «Технічні правила переносу», п. 4: «Не можна розривати умовні (графічні) скорочення на зразок вид-во, і т. д., і т. ін., та ін., т-во тощо»; § 62, п. 2: «У графічних скороченнях пропущену середню частину слова позначаємо дефісом: вид-во (видавництво), гр-н (громадянин), ін-т (інститут), р-н (район), ун-т (університет), ф-ка (фабрика)»",
108
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
109
+ "note": "Це єдина частина hyphen для української, де НЕРОЗРИВНІСТЬ САМА Є НОРМОЮ, а не рішенням проєкту: § 64 п. 4 прямо забороняє розривати цей клас, а § 62 п. 2 закриває його літеральним переліком, бо «тощо» в § 64 залишає клас відкритим. Українська цим відрізняється від польської, де PWN [196] поділ у місці дефіса саме ПРИПИСУЄ, через що pl.json має три порожні списки. Звірено 18.09.2026."
110
+ },
111
+ {
112
+ "rule": "hyphen",
113
+ "cite": "Український правопис (2019), § 42 «Прийменники», п. 2: «З дефісом пишемо складені прийменники, утворені з простих прийменників з, із та інших прийменників: з-за (із-за), з-над, з-перед, з-під (із-під), з-поза, з-поміж, з-понад, з-попід, з-посеред, з-проміж»; § 44 «Частки», п. 3: «З дефісом пишемо: 1) частки -бо, -но, -от, -то, -таки … 2) частки будь-, -будь, -небудь, казна-, хтозна-, бозна- із займенниками і прислівниками»",
114
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
115
+ "note": "Ці параграфи нормативні для СКЛАДУ форм, але не для заборони переносу, і змішувати це не можна — так само, як у ru.json. §§ 63 і 64 прочитано повністю: § 63 нормує поділ за складами, § 64 забороняє розривати ініціали, назви мір, нарощення й графічні скорочення, але про дефіс у формах §§ 42 і 44 не каже нічого. Отже, для цих класів зв'язування через U+2011 — рішення проєкту (docs/PLAN.md §3.3), а не орфографічна норма, і це записано прямо. НЕ ВНЕСЕНО, свідомо: § 64 п. 3 («Граматичні закінчення, поєднані із цифрами дефісом, не можна відривати й переносити: 2-й, 4-го, 10-му») нормативно нерозривний, але це форма «цифра-дефіс-літера»; чи є це членством у hyphen.suffixes, чи вже алгоритмом, має вирішити spec-guardian. Звірено 18.09.2026."
116
+ },
117
+ {
118
+ "rule": "nbsp",
119
+ "cite": "Український правопис (2019), § 64 «Технічні правила переносу», п. 1: «Не можна переносити прізвища, залишаючи в кінці попереднього рядка ініціали або інші умовні скорочення, що належать до них: Т. Г. Шевченко (не Т. Г. // Шевченко), гр. Іваненко, акад. (доц., проф.) Гончаренко, п. Гнатюк»; п. 4: «Не можна розривати умовні (графічні) скорочення на зразок вид-во, і т. д., і т. ін., та ін., т-во тощо»; п. 5: «Не можна переносити в наступний рядок розділові знаки (крім тире), дужку або лапки, що закривають попередній рядок»",
120
+ "url": "https://www.ulif.org.ua/system/files/pravopus-new.pdf",
121
+ "note": "§ 64 — ЗАКРИТИЙ перелік із п'яти пунктів, і це важливо не лише тим, що він містить, а й тим, чого в ньому немає. (1) initialBinding = «chain»: п. 1 наводить послідовність ДВОХ ініціалів і не дає прикладу одного ініціала перед прізвищем. (2) beforeWord: з п. 1 взято лише «акад.», «доц.», «проф.». «п.» і «гр.» свідомо НЕ внесено за тією самою дисципліною, з якої ru.json виключає «г.»: «п.» — це також «пункт» («п. 3»), «гр.» — також «градус» і «графа», а літеральний список цих напрямків не розрізняє. (3) beforePunctuation і narrowBeforePunctuation порожні: п. 5 прив'язує розділовий знак до ПОПЕРЕДНЬОГО слова — це заперечення, а не мовчання. (4) afterShortWords ПОРОЖНІЙ, і це підтверджена відсутність норми: перелік § 64 закритий, правила про однобуквені прийменники в ньому немає, а в самому правописі «і», «у», «в», «з» регулярно стоять у кінці рядка. Список ru.json сюди НЕ переноситься за аналогією, хоча мови споріднені. (5) afterSymbols і beforeNumber порожні: конструкцій «символ + число» і «скорочення + число» § 64 не містить. Звірено 18.09.2026."
122
+ },
123
+ {
124
+ "rule": "nbsp",
125
+ "cite": "Український правопис (2019), § 64, п. 2: «Не можна відривати скорочені назви мір від цифр, до яких вони належать: 2008 р. (не 2008 // р.), 150 га (не 150 // га), 20 см³ або 20 куб. см, 5 г (не 5 // г)»; § 62: «Скорочені назви одиниць вимірювання пишемо без крапок: Б — байт, Вт — ват, г — грам, га — гектар, год — година, дм — дециметр, кБ — кілобайт, кВт — кіловат, кг — кілограм, км — кілометр, л — літр, м — метр, мм — міліметр, с — секунда, см — сантиметр, т — тонна, хв — хвилина, ц — центнер»; BIPM, The International System of Units (SI), 9th ed., concise summary: «A single space is always left between the number and the unit»",
126
+ "url": "https://www.bipm.org/documents/20126/41483022/SI-Brochure-9-concise-EN.pdf",
127
+ "note": "Розподіл ролей тут ІНШИЙ, ніж у pl, fi, sv і en-US: там BIPM дає перелік символів, а національне джерело — конвенцію відступу; для української національне джерело дає і те, і те, бо § 64 п. 2 нормує саме невідривність. BIPM наведено як підтвердження загального принципу, а не як несуча цитата. Однобуквені позначення (г, л, м, с, т, ц, Б) свідомо пропущено: правий кордон N5 вимагає лише, щоб cp[a+k] не належав ALNUM, і пробіл цю умову задовольняє, тож однобуквений запис зв'язував би звичайну прозу. «р.» внесено: на відміну від російського «г.», українське «р.» означає тільки «рік» і зв'язується вліво після числа. «%» внесено ЯК РІШЕННЯ, а не як цитата: його немає ні в § 62, ні в § 64, ні в скороченому викладі брошури BIPM (правило про % — у § 5.4.7 повного видання, яке не діставалося), але відбивка відсотка від числа є і в російській локалі, і в усіх інших локалях цього проєкту, а розбіжність тут дала б українській вужчу поведінку без жодної підстави в джерелі. Звірено 18.09.2026."
128
+ }
129
+ ]
130
+ }
@@ -0,0 +1,157 @@
1
+ # `analyze` — the reported-edits entry point
2
+
3
+ **Not a rule.** No entry in `spec/rules/order.json`, no locale data of its own, no edits. This
4
+ document specifies a **second public entry point** beside `transform`, which runs the identical
5
+ pipeline and reports what it would do instead of doing it.
6
+ **Spec version:** 1.3.0 (new in 1.3.0).
7
+
8
+ ---
9
+
10
+ ## 1. Why a second function and not an option
11
+
12
+ `transform` returns a string. A `dryRun: true`-style option would make the **return type depend
13
+ on the value of an argument**, and that is not portable across the five runtimes this project
14
+ targets: TypeScript could express it with overloads, but Go's `Transform(string, Options)
15
+ (string, error)` has no room for a second result shape, PHP would have to declare
16
+ `string|array`, and Python `str | list[Change]` — a union every caller must narrow before it can
17
+ use either half. One function, one return type, in all five.
18
+
19
+ So `transform` is untouched and keeps its signature. `analyze` is a sibling:
20
+
21
+ ```
22
+ transform(input, options) -> string
23
+ analyze(input, options) -> Change[]
24
+ ```
25
+
26
+ Same `options`, same validation, same errors, same purity (`ARCHITECTURE.md` §7: no I/O, no
27
+ clock, no globals, reentrant). Everything §2 of every rule document says about what the pipeline
28
+ does applies unchanged — `analyze` **is** the pipeline; it merely keeps the edits instead of
29
+ discarding them after applying.
30
+
31
+ This is the feature `ARCHITECTURE.md` §7.1 reserved the engine's shape for: *rules produce edits,
32
+ the pipeline applies them*. Nothing in the engine changes to support it.
33
+
34
+ ---
35
+
36
+ ## 2. What a `Change` is
37
+
38
+ ```
39
+ Change {
40
+ ruleId a rule id from spec/rules/order.json
41
+ start code-point offset into `input`, inclusive
42
+ end code-point offset into `input`, exclusive
43
+ before the text this rule replaced — empty for a pure insertion
44
+ after the text it replaced it with — empty for a pure deletion
45
+ }
46
+ ```
47
+
48
+ - **Offsets are code points, never native string indices** (`ARCHITECTURE.md` §4.2). A runtime
49
+ whose strings are UTF-16 must convert; a runtime whose strings are bytes must convert.
50
+ - **Offsets are into `input` exactly as the caller passed it.** In `html`, `markdown` and
51
+ `yaml` mode that means offsets into the **document**, not into the span the rules actually ran
52
+ over. This
53
+ is not a new obligation: [modes.md](modes.md) §4 already defines the output as "the input
54
+ source with a set of disjoint substring replacements applied **at recorded offsets**", and
55
+ those are the offsets meant.
56
+ - `start == end` is a pure insertion; `before` is then empty. `after` empty with `end > start`
57
+ is a pure deletion. Both occur: `nbsp` inserts, `spaces` deletes.
58
+
59
+ ---
60
+
61
+ ## 3. Order
62
+
63
+ Changes are reported in **pipeline order**: rules in the order `spec/rules/order.json` declares,
64
+ and within one rule ascending by `start`. That is the order in which the work actually happened,
65
+ and it is the order a reader needs to understand a result — `spaces` deleting a space and `nbsp`
66
+ putting a no-break one back at the same index is intelligible in that order and baffling in any
67
+ other.
68
+
69
+ ---
70
+
71
+ ## 4. What is contract and what is observation
72
+
73
+ This is the part to read before building anything on top.
74
+
75
+ **Contract, conformance-tested:**
76
+
77
+ - **A1.** `analyze` accepts exactly what `transform` accepts and rejects exactly what it rejects,
78
+ with the same error codes — including `POLYTYPO_MALFORMED_INPUT` for a document that does not
79
+ parse as its declared dialect.
80
+ - **A2.** `analyze` is pure and returns the same list for the same arguments, always.
81
+ - **A3.** The list is **empty if and only if** `transform(input, options) == input`. A document
82
+ that needs nothing produces no changes; a document that produces no changes needs nothing.
83
+ - **A4.** Every `ruleId` is a rule that was **enabled for that call** — a rule turned off through
84
+ `rules`, and `ranges` when it was not turned on, can never appear.
85
+ - **A5.** Every `start` and `end` is within `0 … length(input)` in code points, and
86
+ `start <= end`.
87
+
88
+ **Observation, not conformance-tested:**
89
+
90
+ - **The decomposition itself.** How a runtime splits one visible change into `Change` records —
91
+ one edit or two, where exactly a boundary falls when two rules touch adjacent characters — is
92
+ that runtime's report of its own work. Five runtimes are **not** required to produce
93
+ identical lists, and no fixture asserts one.
94
+
95
+ That line is drawn deliberately. Making the decomposition contract would freeze the internal
96
+ shape of every rule forever: two rules touching adjacent code points would have to agree, across
97
+ five languages, on how many edits that is. The cost is real and the benefit is not — a caller
98
+ wants to know *what changes and which rule did it*, which A1–A5 give.
99
+
100
+ **The output of `transform` remains the only byte-level contract.** If a caller needs the
101
+ transformed text, the way to get it is to call `transform`.
102
+
103
+ ---
104
+
105
+ ## 5. Changes may overlap, and a naive patch does not reconstruct the output
106
+
107
+ The pipeline is sequential: each rule sees the text the previous rules left. Two rules may
108
+ therefore touch the **same original range**, and both changes are reported, both in original
109
+ coordinates.
110
+
111
+ The standing example is French, where `spaces` (order 10) deletes the space before `:` and
112
+ `nbsp` (order 70) inserts U+00A0 at the same place:
113
+
114
+ | | ruleId | start | end | before | after |
115
+ | --- | --- | --- | --- | --- | --- |
116
+ | 1 | `spaces` | 3 | 4 | `␣` | |
117
+ | 2 | `nbsp` | 4 | 4 | | `⍽` |
118
+
119
+ Applying that list to the input as if it were a patch — even in order, even accumulating
120
+ offsets — is **not** guaranteed to reproduce `transform`'s output, and this specification does
121
+ not promise that it does. A consumer that wants the output calls `transform`; a consumer that
122
+ wants to show a reviewer what will change uses the list as the report it is.
123
+
124
+ An implementation **must not** silently merge or drop changes to make the list patchable. A
125
+ report that omits work the pipeline did is worse than one a caller cannot replay.
126
+
127
+ ---
128
+
129
+ ## 6. Conformance
130
+
131
+ No new fixture format. `spec/fixtures/*.json` keeps its `in`/`out` shape, and `analyze` is not
132
+ expressible in it — a fixture asserting a specific `Change[]` would be asserting the very
133
+ decomposition §4 declines to make contract.
134
+
135
+ Each runtime proves A1–A5 with its own tests, and the two that are cheap to get wrong are worth
136
+ naming:
137
+
138
+ - **A3 against the whole fixture corpus.** For every canonical fixture case, `analyze` returns
139
+ an empty list exactly when `in == out`. That is a strong test and it costs one loop.
140
+ - **A5 under the mode adapters.** A runtime that reports span-local offsets in `html`,
141
+ `markdown` or `yaml` mode passes every text-mode test and is still wrong. Test a document
142
+ whose first span does not start at offset 0. `yaml` is the cheapest of the three to get wrong
143
+ and the cheapest to test: no span in it ever starts at offset 0, since every one of them is
144
+ preceded by at least a key and a colon.
145
+
146
+ ---
147
+
148
+ ## 7. Open questions
149
+
150
+ 1. **A contract-level decomposition, if anyone ever needs one.** §4 makes the split an
151
+ observation. If a consumer appears that genuinely needs byte-identical `Change[]` across
152
+ runtimes — a distributed review tool, say, diffing one runtime's report against another's —
153
+ that is a later, larger spec change: it would need a canonical edit-merging rule and fixtures
154
+ in a new format. Nothing in this document forecloses it; A1–A5 stay true either way.
155
+ 2. **`analyze` over a document with no spans.** `html` mode on a document that is markup from
156
+ end to end returns an empty list, which A3 already requires, since `transform` returns the
157
+ input unchanged. Recorded because it reads like an edge case and is not one.