Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bfa449b
First version of Russian Braille
Kostenkov-2021 Jun 1, 2026
ae72c65
typo in example
NSoiffer Jun 1, 2026
b6ef68d
Russian Braille fixes
Kostenkov-2021 Jun 3, 2026
b2de4ca
Braille - Latin and Greek letters fixes
Kostenkov-2021 Jun 3, 2026
80f1edd
Braille - Roots, powers and scripts
Kostenkov-2021 Jun 3, 2026
efe10da
Update Russian Braille rules and mappings
Kostenkov-2021 Jun 3, 2026
e298a6d
Add Russian language to docs
Kostenkov-2021 Jun 11, 2026
067770f
Refactor Russian function braille mapping
Kostenkov-2021 Jun 12, 2026
9720f49
Refine Russian wording in translators guide
Kostenkov-2021 Jun 24, 2026
70eed4e
Addition of Russian documentation
Kostenkov-2021 Jun 1, 2026
1afd36f
First version of Russian Braille
Kostenkov-2021 Jun 1, 2026
d16e42b
typo in example
NSoiffer Jun 1, 2026
3cd8634
Add alphabet indicators in russian_cleanup
Kostenkov-2021 Jun 25, 2026
5d430b8
Russian braille: add GOST symbols and decimals
Kostenkov-2021 Jun 25, 2026
8319ee7
Add vector/bar/arc mover rules and tests
Kostenkov-2021 Jun 25, 2026
75a3a43
Add Russian braille math functions and limits
Kostenkov-2021 Jun 25, 2026
14f61d9
Support system left brace in Russian braille
Kostenkov-2021 Jun 25, 2026
68e73a7
Add Russian chemistry braille rules and tests
Kostenkov-2021 Jun 25, 2026
c533887
Add signed-integer and nested-fraction rules
Kostenkov-2021 Jun 25, 2026
1f6de0d
Enhance Russian Braille support with mathematical symbols and rules (…
Kostenkov-2021 Jun 25, 2026
6161ccf
Add Russian braille label marks and tests
Kostenkov-2021 Jun 29, 2026
f659338
Add Russian braille support for periodic decimals and mixed numbers
Kostenkov-2021 Jun 29, 2026
3f84b47
Add Russian braille rules for geometry operators
Kostenkov-2021 Jun 29, 2026
25f17c6
Adjust Russian braille union handling and add tests
Kostenkov-2021 Jun 29, 2026
c996301
Improve Russian braille typeform and mathvariant handling
Kostenkov-2021 Jun 29, 2026
2bdb2dc
Add Russian Braille chemistry notation rules
Kostenkov-2021 Jun 29, 2026
f245976
Add Russian braille mappings and tests
Kostenkov-2021 Jun 29, 2026
2062a0a
Add column arithmetic and enhance Russian braille rules
Kostenkov-2021 Jun 30, 2026
97b5b5a
Optimize braille string operations for single chars
Kostenkov-2021 Jul 2, 2026
4b09632
Verify RU speech strings; add nav & unit rules
Kostenkov-2021 Jul 3, 2026
88c100b
Add Russian braille rules for geometry accents
Kostenkov-2021 Jul 5, 2026
46ea5db
Add vector and long-division Russian braille rules
Kostenkov-2021 Jul 5, 2026
d77a225
Add postscripts and new label marks for Russian Braille
Kostenkov-2021 Jul 5, 2026
94390e4
Fix Russian percent braille and add per mille sign
Kostenkov-2021 Jul 21, 2026
b1fdd49
Propagate mathvariant attribute to children
Kostenkov-2021 Jul 21, 2026
bba0622
Fix clippy warnings and improve code style
Kostenkov-2021 Jul 21, 2026
fe3f717
Remove unnecessary borrows in format macros
Kostenkov-2021 Jul 21, 2026
6dd51df
Normalize Unicode math digits in decimal numbers
Kostenkov-2021 Jul 21, 2026
76d19a2
Remove unnecessary references and .to_string() calls
Kostenkov-2021 Jul 21, 2026
cf79163
Remove mathvariant propagation and update tests
Kostenkov-2021 Jul 31, 2026
75be354
Remove math digit normalization for mn nodes
Kostenkov-2021 Aug 1, 2026
fdd9e1a
Merge branch 'ru' of https://github.com/Kostenkov-2021/MathCAT into ru
Kostenkov-2021 Aug 3, 2026
f4a35bf
Split Russian braille tests into individual cases
Kostenkov-2021 Aug 3, 2026
27d6894
Adjust test expectations and unignore tests
Kostenkov-2021 Aug 3, 2026
f99bf16
Bump to 0.7.6-beta.8 and update dependencies
Kostenkov-2021 Aug 3, 2026
c3042f9
Switch to sxd_document_no_unsafe and NameStr
Kostenkov-2021 Aug 3, 2026
5701054
Use no-unsafe sxd crates; adapt code & tests
Kostenkov-2021 Aug 3, 2026
b104f46
Use no-unsafe sxd libs; refactor TTS & parsing
Kostenkov-2021 Aug 3, 2026
6901161
Merge branch 'ru' into ru
NSoiffer Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,3 @@ Rules/*/*/*.zip
docs/_site
docs/.jekyll-cache
docs/Gemfile.lock

docs/nb_of_norway/
29 changes: 7 additions & 22 deletions Rules/Languages/en/SharedRules/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
- test:
- if: "string-length(.) = 1 and text() != '_'" # need unicode.tdl to kick in for single letter tokens
then: [x: "text()"]
- else_if: "@data-chem-element > 0 or @data-roman-numeral" # NavMode=Character needs this
- else_if: "@data-chem-element or @data-roman-numeral" # NavMode=Character needs this
then: [spell: "text()", pause: "short"]
else: [x: "translate(., '-_\u00A0', ' ')"] # from intent literals or from extra spaces added (which get deleted)

Expand Down Expand Up @@ -237,26 +237,12 @@
- x: "*[2]"
- t: "below" # phrase(x with z 'below' it)

# FIX: this only works for up to four dots (which is realistically all we need)
- name: dot-over
tag: modified-variable
match: "*[1][self::m:mi] and (*[2][translate(., '.', '')=''])"
replace:
- x: "*[1]"
- x: "IfThenElse(string-length(*[2])=1, 'dot',
IfThenElse(string-length(*[2])=2, 'double dot',
IfThenElse(string-length(*[2])=3, 'triple dot', 'quadruple dot')
))"
- pause: short

# FIX: the 'translate' list is a little different from 'modified-variable', so we also include 'mover' (unify?)
- name: diacriticals
tag: [mover, modified-variable]
tag: mover
match: "*[1][self::m:mi] and *[2][translate(., '\u0306\u030c.\u00A8\u02D9\u20DB\u20DC`^ˇ~→¯_', '')='']"
replace:
- x: "*[1]"
- x: "*[2]"
- pause: short

- name: default
tag: mover
Expand Down Expand Up @@ -436,23 +422,22 @@
- t: "end scripts" # phrase(At this point 'end scripts' occurs)

- name: default
tag: [mtable, array]
tag: mtable
variables:
- IsColumnSilent: "false()"
- NumColumns: "CountTableColumns(.)"
- NumRows: "CountTableRows(.)"
- NumColumns: "count(*[1]/*) - IfThenElse(*/self::m:mlabeledtr, 1, 0)"
match: "."
replace:
- t: "table with" # phrase(the 'table with' 3 rows)
- x: "$NumRows"
- x: count(*)
- test:
if: "$NumRows=1"
if: count(*)=1
then: [t: "row"] # phrase(the table with 1 'row')
else: [t: "rows"] # phrase(the table with 3 'rows')
- t: "and" # phrase(the table with 3 rows 'and' 4 columns)
- x: "$NumColumns"
- test:
if: "$NumColumns=1"
if: "NumColumns=1"
then: [t: "column"] # phrase(the table with 3 rows and 1 'column')
else: [t: "columns"] # phrase(the table with 3 rows and 4 'columns')
- pause: long
Expand Down
4 changes: 3 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ fn zip_entry<W: Write + Seek>(
/// Zip up each language and braille dir
/// Note: regional variations (including zh-cn and zh-tw) are zipped together into one zip file
fn main() {
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed=Rules");

// This doesn't work because the build claims OUT_DIR is not defined(?)
// let archive = PathBuf::from(concat!(env!("OUT_DIR"),"/rules.zip"));
if std::env::var("CARGO_FEATURE_INCLUDE_ZIP").is_ok() {
Expand Down Expand Up @@ -198,6 +201,5 @@ fn main() {
if let Err(e) = archive_zip.finish() {
panic!("Error in zip.finish(): {}", e);
}
println!("cargo::rerun-if-changed=Rules");
}
}
44 changes: 23 additions & 21 deletions src/braille.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,13 +446,12 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz
/// find the navigation node that most tightly encapsulates the target position (0-based)
/// 'node' is the current node we are on inside of 'mathml'
fn find_navigation_node<'e>(mathml: Element<'e>, node: Element<'e>, target_position: usize) -> Result<SearchState<'e>> {
let raw_node_id = node.attribute_value("id");
let node_id = match raw_node_id.as_deref() {
let node_id = match node.attribute_value("id") {
Some(id) => id,
None => bail!("'id' is not present on mathml: {}", mml_to_string(node)),
};
N_PROBES.with(|n| {*n.borrow_mut() += 1});
let (braille, char_start, char_end) = braille_mathml(mathml, node_id)?;
let (braille, char_start, char_end) = braille_mathml(mathml, as_str!(node_id))?;
let mut status = None;
// debug!("find_navigation_node ({}, id={}): highlight=[{}, {}); target={}", name(node), node_id, char_start, char_end, target_position);
if is_leaf(node) {
Expand Down Expand Up @@ -519,7 +518,7 @@ pub fn get_navigation_node_from_braille_position(mathml: Element, position: usiz
return Ok(status);
},
SearchStatus::LookInParent => {
let (_, start, end) = braille_mathml(mathml, node_id)?;
let (_, start, end) = braille_mathml(mathml, as_str!(node_id))?;
// debug!(" parent ({}) braille: start/end={}/{}; target_position={}", name(node), start, end, target_position);
if start <= target_position && target_position < end {
// debug!(" ..found: id={}", node_id);
Expand Down Expand Up @@ -2633,7 +2632,7 @@ impl BrailleChars {
// also true (sort of) for capitalization -- if all caps, use double cap in front (assume abbr or Roman Numeral)

// we only care about this for numbers and identifiers/text, so we filter for only those
let node_name = name(node);
let node_name = as_str!(name(node));
let is_in_enclosed_list = node_name != "mo" && BrailleChars::is_in_enclosed_list(node);
let is_mn_in_enclosed_list = is_in_enclosed_list && node_name == "mn";
let mut typeface = "R".to_string(); // assumption is "R" and if attr or letter is different, something happens
Expand Down Expand Up @@ -2917,8 +2916,8 @@ impl BrailleChars {
"mo" => !crate::canonicalize::is_relational_op(node),
"mtext" => {
let raw_text = as_text(node);
let text = raw_text.trim();
return text=="?" || text=="-?-" || text.is_empty();
let text = as_str!(raw_text).trim();
return text=="?" || text=="-?-" || text.is_empty(); // various forms of "fill in missing content" (see also Nemeth_RULEs.yaml, "omissions")
},
"mrow" => {
if IsBracketed::is_bracketed(node, "", "", false, false) {
Expand All @@ -2933,7 +2932,7 @@ impl BrailleChars {
true
},
"menclose" => {
if let Some(notation) = node.attribute_value("notation").as_deref() {
if let Some(notation) = node.attribute_value("notation") {
if notation != "bottom" || notation != "box" {
return false;
}
Expand Down Expand Up @@ -3021,7 +3020,7 @@ impl NeedsToBeGrouped {
// ordinals often have an irregular start (e.g., "half") before becoming regular.
// if the number is irregular, return the ordinal form, otherwise return 'None'.
fn needs_grouping_for_cmu(element: Element, _is_base: bool) -> bool {
let node_name = name(element);
let node_name = as_str!(name(element));
let children = element.children();
if node_name == "mrow" {
// check for bracketed exprs
Expand Down Expand Up @@ -3076,10 +3075,11 @@ impl NeedsToBeGrouped {
/// FIX: what needs to be implemented?
fn needs_grouping_for_finnish(mathml: Element, is_base: bool) -> bool {
use crate::xpath_functions::IsInDefinition;
let mut node_name = as_str!(name(mathml));
if mathml.attribute_value("data-roman-numeral").is_some() {
node_name = "mi"; // roman numerals don't follow number rules
}
let node_name = if mathml.attribute_value("data-roman-numeral").is_some() {
"mi" // roman numerals don't follow number rules
} else {
as_str!(name(mathml))
};

// FIX: the leaf rules are from UEB -- check the Swedish rules
match node_name {
Expand Down Expand Up @@ -3167,10 +3167,11 @@ impl NeedsToBeGrouped {
// if the number is irregular, return the ordinal form, otherwise return 'None'.
fn needs_grouping_for_swedish(mathml: Element, is_base: bool) -> bool {
use crate::xpath_functions::IsInDefinition;
let mut node_name = as_str!(name(mathml));
if mathml.attribute_value("data-roman-numeral").is_some() {
node_name = "mi"; // roman numerals don't follow number rules
}
let node_name = if mathml.attribute_value("data-roman-numeral").is_some() {
"mi" // roman numerals don't follow number rules
} else {
as_str!(name(mathml))
};

match node_name {
"mn" => return false,
Expand Down Expand Up @@ -3234,10 +3235,11 @@ impl NeedsToBeGrouped {
// 8. If none of the foregoing apply, the item is simply the [this element's] individual symbol.

use crate::xpath_functions::IsInDefinition;
let mut node_name = as_str!(name(mathml));
if mathml.attribute_value("data-roman-numeral").is_some() {
node_name = "mi"; // roman numerals don't follow number rules
}
let node_name = if mathml.attribute_value("data-roman-numeral").is_some() {
"mi" // roman numerals don't follow number rules
} else {
as_str!(name(mathml))
};
match node_name {
"mn" => {
if !is_base {
Expand Down
6 changes: 2 additions & 4 deletions src/canonicalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5535,7 +5535,6 @@ mod canonicalize_tests {
}

#[test]
#[ignore] // need to figure out a test for this ("|" should have a precedence around ":" since that is an alternative notation for "such that", but "∣" is higher precedence)
fn vertical_bar_divides() -> Result<()> {
let test_str = "<math>
<mi>x</mi><mo>+</mo><mi>y</mi> <mo>|</mo><mn>12</mn>
Expand All @@ -5547,7 +5546,7 @@ mod canonicalize_tests {
<mo>+</mo>
<mi>y</mi>
</mrow>
<mo>∣ <!--divides--></mo>
<mo>|</mo>
<mn>12</mn>
</mrow>
</math>";
Expand Down Expand Up @@ -6896,7 +6895,6 @@ mod canonicalize_tests {


#[test]
#[ignore] // this fails -- need to figure out grabbing base from previous or next child
fn tensor() -> Result<()> {
let test_str = "<math>
<msub><mi>R</mi><mi>i</mi></msub>
Expand All @@ -6913,7 +6911,7 @@ mod canonicalize_tests {
<mi> j </mi>
<mi> k </mi>
<none/>
<mi> l </mi>
<mi data-chem-state='true'> l </mi>
<none/>
</mmultiscripts>
</math>";
Expand Down
1 change: 0 additions & 1 deletion src/chemistry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,7 +2143,6 @@ mod chem_tests {
}

#[test]
#[ignore] // It would be good to say "not chemistry" for this, but there aren't rules for that at the moment
fn test_water_bond() -> Result<()> {
return chem_test(|| {
let test11 = r#"<mrow><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mi mathvariant='normal'>O</mi><mo>=</mo><mi>O</mi></mrow>"#;
Expand Down
6 changes: 3 additions & 3 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,11 +1365,11 @@ mod tests {
set_mathml(good_mathml)?;
let bad_mathml = "<math><mi>&xabc;</mi></math>";
assert!(set_mathml(bad_mathml).is_err());
assert!(get_spoken_text()? == "");
assert!(get_spoken_text()?.is_empty());
set_mathml(good_mathml)?;
let bad_mathml = "<math>garbage";
assert!(set_mathml(bad_mathml).is_err());
assert!(get_spoken_text()? == "");
assert!(get_spoken_text()?.is_empty());
return Ok( () );
});
}
Expand All @@ -1396,7 +1396,7 @@ mod tests {
<mrow> <mi>x</mi><mo>-</mo><mi>y</mi> </mrow>
</mfrac>
</math>";
set_mathml(&expr)?;
set_mathml(expr)?;
let speech = get_spoken_text()?;
// Rule-generated SSML must pass through verbatim (not XML-entity-encoded).
assert!(!speech.contains("&lt;"));
Expand Down
4 changes: 2 additions & 2 deletions src/navigate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,10 +1009,10 @@ mod tests {
match do_navigate_command_string(mathml, command) {
Err(e) => {
panic!("\nStarting at '{}', '{} failed.\n{}",
start_id, command, &crate::interface::errors_to_string(&e))
start_id, command, crate::interface::errors_to_string(&e))
},
Ok(nav_speech) => {
let nav_speech = nav_speech.trim_end_matches(&[' ', ',', ';']);
let nav_speech = nav_speech.trim_end_matches([' ', ',', ';']);
// debug!("Full speech: {}", nav_speech);
if !result_id.is_empty() {
let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml);
Expand Down
52 changes: 48 additions & 4 deletions src/prefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,54 @@ mod tests {
return stripped_path
}

struct TestRulesDir {
path: PathBuf,
}

impl TestRulesDir {
fn path(&self) -> &Path {
return &self.path;
}
}

impl Drop for TestRulesDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}

fn copy_dir_recursive(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for entry in std::fs::read_dir(from).unwrap() {
let entry = entry.unwrap();
let file_type = entry.file_type().unwrap();
let target = to.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), target).unwrap();
}
}
}

fn test_rules_dir_with_prefs() -> TestRulesDir {
let path = std::env::temp_dir().join(format!("mathcat-prefs-test-{}", fastrand::u64(..)));
std::fs::create_dir_all(&path).unwrap();

let source_rules = abs_rules_dir_path();
let prefs = std::fs::read_to_string(source_rules.join("prefs.yaml")).unwrap()
.replace("Language: Auto", "Language: zz");
std::fs::write(path.join("prefs.yaml"), prefs).unwrap();
std::fs::copy(source_rules.join("intent.yaml"), path.join("intent.yaml")).unwrap();
std::fs::copy(source_rules.join("definitions.yaml"), path.join("definitions.yaml")).unwrap();

copy_dir_recursive(&source_rules.join("Languages").join("en"), &path.join("Languages").join("en"));
copy_dir_recursive(&source_rules.join("Languages").join("zz"), &path.join("Languages").join("zz"));
copy_dir_recursive(&source_rules.join("Braille").join("Nemeth"), &path.join("Braille").join("Nemeth"));

return TestRulesDir { path };
}

fn speech_rule_files_cache_is_empty() -> bool {
crate::speech::SPEECH_RULES.with(|rules| rules.borrow().rule_files_cache_is_empty())
}
Expand Down Expand Up @@ -1231,7 +1279,6 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
}

#[test]
#[ignore] // this is an ugly test for #262 -- it changes the prefs file and so is a bad thing in general
fn test_up_to_date() {
use std::fs;
use std::thread::sleep;
Expand Down Expand Up @@ -1261,9 +1308,6 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
});
assert_eq!(interface::get_spoken_text().unwrap(), "SimpleSpeak positive from zz 10");
fs::write(&file_path, contents).unwrap();

// assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
// assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
}

}}
Expand Down
3 changes: 2 additions & 1 deletion src/speech.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3027,7 +3027,8 @@ cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone();

// open the file, read all the contents, then write them back so the time changes
let contents = std::fs::read(&start_main_file.file).unwrap_or_else(|_| panic!("Failed to read file {} during test", start_main_file.file.to_string_lossy()));
let contents = std::fs::read(&start_main_file.file)
.unwrap_or_else(|_| panic!("Failed to read file {} during test", start_main_file.file.to_string_lossy()));
std::fs::write(start_main_file.file, contents).unwrap();
std::thread::sleep(Duration::from_millis(5)); // pause a little to make sure the time changes

Expand Down
8 changes: 4 additions & 4 deletions tests/Languages/en/mtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,7 @@ fn matrix_simple_table() -> Result<()> {
let expr = "<math>
<mtable><mtr><mtd><mn>3</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd></mtr></mtable>
</math>";
test("en", "ClearSpeak", expr, "table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2")
test("en", "ClearSpeak", expr, "array of; row 1; column 1; 3; comma; row 2; column 1; 2")
}

#[test]
Expand All @@ -1149,7 +1149,7 @@ fn mtable_blank_op() -> Result<()>{
let expr = "<math>
<mo>\u{2062}</mo><mtable><mtr><mtd><mn>3</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd></mtr></mtable>
</math>";
test("en", "ClearSpeak", expr, "; table with 2 rows and 1 column; row 1; column 1; 3; row 2; column 1; 2")
test("en", "ClearSpeak", expr, "; array of; row 1; column 1; 3; comma; row 2; column 1; 2")
}


Expand All @@ -1159,7 +1159,7 @@ fn mtable_colspan_table() -> Result<()>{
let expr = "<math>
<mtable><mtr><mtd colspan=\"2\"><mn>3</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd><mtd><mn>4</mn></mtd></mtr></mtable>
</math>";
test("en", "ClearSpeak", expr, "table with 2 rows and 2 columns; row 1; column 1; 3; row 2; column 1; 2, column 2; 4")
test("en", "ClearSpeak", expr, "array of; row 1; column 1; 3; comma; row 2; column 1; 2, column 2; 4")
}

#[test]
Expand All @@ -1180,7 +1180,7 @@ fn bug_mtable_rowspan_colspan() -> Result<()>{
</mtable>
</math>";
test("en", "ClearSpeak", expr,
"table with 2 rows and 5 columns; row 1; column 1; eigh, column 2; b, column 3; c; row 2; column 1; d, column 2; e")
"array of; row 1; column 1; eigh, column 2; b, column 3; c; comma; row 2; column 1; d, column 2; e")
}


Expand Down
Loading
Loading