1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
macro_rules! encode_double_quote_arm {
($tokens:ident, $out:ident, $prev_was_number:ident, $numeric_mode:ident, $quote_open:ident, $internal_double_quote_open:ident, $passage:ident, $regex_listing:ident, $i:ident) => {
{
if $regex_listing[$i] {
// RUEB 2024 §7.6.5: straight quotes used as ASCII regex
// characters are nondirectional quote signs, not the
// surrounding prose quotation marks.
$out.extend([decode_unicode('⠠'), decode_unicode('⠶')]);
$prev_was_number = false;
$numeric_mode = false;
continue;
}
if let Some((end, form, caps, _)) = $passage
&& end == $i + 1
&& matches!($i.checked_sub(1).and_then(|p| $tokens.get(p)), Some(EnglishToken::Symbol(',')))
{
// §13.2/§9: when a foreign/typeform phrase includes its
// trailing comma before a closing quotation mark, the
// typeform terminator closes the phrase before the quote.
if caps {
$out.extend([CAPITAL, decode_unicode('⠄')]);
}
$out.extend(super::rule_9::terminator(form));
$passage = None;
}
if matches!($i.checked_sub(1).and_then(|p| $tokens.get(p)), Some(EnglishToken::Number(_))) {
// §3.15.1: a straight double quote after a number is the
// inch mark, not a directional quotation mark.
$out.extend([decode_unicode('⠠'), decode_unicode('⠶')]);
$prev_was_number = false;
$numeric_mode = false;
continue;
}
if $tokens.iter().enumerate().any(|(idx, _)| straight_single_quote_exchanged($tokens, idx)) {
if $internal_double_quote_open {
$out.extend([decode_unicode('⠠'), decode_unicode('⠴')]);
$internal_double_quote_open = false;
} else {
$out.extend([decode_unicode('⠠'), decode_unicode('⠦')]);
$internal_double_quote_open = true;
}
$prev_was_number = false;
$numeric_mode = false;
continue;
}
// §7.6.10: a double quotation mark standing alone (a space or
// text edge on both sides) is the mark referenced in isolation
// → grade-1 + the nondirectional double-quote sign ⠰⠠⠶, and it
// does not flip the open/close alternation.
let standalone = ($i == 0
|| matches!($tokens.get($i - 1), Some(EnglishToken::Space)))
&& matches!($tokens.get($i + 1), None | Some(EnglishToken::Space));
// §7.6.11 / §2.6 nondirectional: a straight `"` count of 1 in
// the whole token stream is unmatched — it cannot be an opening
// or closing mark of a pair — so it takes the nondirectional
// sign ⠠⠶ (`"yr-123` → ⠠⠶…, `X' Y"` → …⠠⠶). Only fires when the
// input carries exactly one straight `"`; a paired `"…"` still
// uses the directional ⠦/⠴ alternation below.
let straight_quote_count = $tokens
.iter()
.filter(|t| matches!(t, EnglishToken::Symbol('"')))
.count();
let unmatched = straight_quote_count == 1;
let prev_text = matches!($i.checked_sub(1).and_then(|p| $tokens.get(p)), Some(EnglishToken::Word(_) | EnglishToken::Styled(..)));
let next_text = matches!($tokens.get($i + 1), Some(EnglishToken::Word(_) | EnglishToken::Styled(..) | EnglishToken::WordDivision { .. }));
if !$quote_open && prev_text && next_text {
$out.extend([decode_unicode('⠘'), QUOTE_OPEN]);
$quote_open = true;
$internal_double_quote_open = true;
} else if $internal_double_quote_open && prev_text {
$out.extend([decode_unicode('⠘'), QUOTE_CLOSE]);
$quote_open = false;
$internal_double_quote_open = false;
} else if standalone {
$out.extend([GRADE1, decode_unicode('⠠'), decode_unicode('⠶')]);
} else if unmatched
&& !prev_text
&& matches!($tokens.get($i + 1), Some(EnglishToken::Word(_) | EnglishToken::WordDivision { .. }))
&& matches!($tokens.get($i + 2), Some(EnglishToken::Symbol('-')))
&& matches!($tokens.get($i + 3), Some(EnglishToken::LineBreak))
{
// §7.6 with §10.13: a lone print quote at the beginning of a
// divided word (`"In-\ndepth`) is still an opening quotation
// mark. It is unmatched only because the quoted extract
// continues beyond the testcase snippet.
$out.push(QUOTE_OPEN);
$quote_open = true;
} else if unmatched
&& matches!($i.checked_sub(1).and_then(|p| $tokens.get(p)), Some(EnglishToken::Symbol('.' | '?' | '!')))
{
$out.push(QUOTE_CLOSE);
} else if unmatched {
// §7.6.11 nondirectional: an unmatched straight `"`
// attached to a word (`"yr-123`, `X' Y"`) takes ⠠⠶
// without the standalone grade-1 indicator.
$out.extend([decode_unicode('⠠'), decode_unicode('⠶')]);
} else {
// §7.6 double quotation mark: open ⠦ / close ⠴, alternating.
let attached_closing = !$quote_open
&& $i > 0
&& !matches!($tokens.get($i - 1), Some(EnglishToken::Space))
&& !matches!(
$tokens.get($i - 1),
Some(EnglishToken::Symbol('(' | '[' | '{'))
);
$out.push(if $quote_open || attached_closing {
QUOTE_CLOSE
} else {
QUOTE_OPEN
});
$quote_open = !$quote_open;
}
$prev_was_number = false;
$numeric_mode = false;
}
};
}