mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
* Add craft-wordmodel: WordCraft's document model without its editing code
WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.
Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.
* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs
The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.
* craft-mime: detect, for bytes that name no character set
is_text, decode (BOM, unmarked UTF-16, UTF-8, else the legacy encoding
the bytes look like), legacy and legacy_label, behind a detect feature
that brings in chardetng. Moved from WordCraft's crates/formats/src/txt.rs
at 06c2ae6f with its text-sniffing tests. Also charset::is_known, so a
caller can tell a label decode would read as UTF-8 from one it knows.
* craft-wordmodel: keep the document operations (edit.rs) with the model
The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.
base64 0.23, as WordCraft and craft-offcrypto use.
* craft-codepage: one table per legacy code page, for craft-rtf, craft-dxf and craft-mime
craft-rtf and craft-dxf each carried tables for DOS 437, 737, 850, 852 and
857 (and craft-rtf one for 866). They are now one set in craft-codepage,
which takes craft-rtf's codepage module (Windows and Mac tables, OEM glyphs,
font charsets) and craft-dxf's DOS tables. craft_rtf::codepage re-exports it
unchanged; craft-dxf keeps its $DWGCODEPAGE names and Decoder and looks the
DOS tables up there.
Every byte of every code page either crate decodes is unchanged: the
public decoders of both crates were dumped over all 256 bytes of each page
(and the East Asian pairs) before and after, and the dumps are identical.
craft-mime now reads the DOS pages by their IANA names (IBM437, cp850,
ibm-852, 857...), which it read as UTF-8 before.
* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath
The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.
mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.
* craft-math: read MathLive's and other spellings of LaTeX math commands
\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).
* craft-math: equaSHion's equation corpus as test data
corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.
* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README
The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.
* Add craft-flow: the flow model the text formats map to, on craft-wordmodel
WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow,
FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and
from_doc, picture sniffing, colours, base64 and the Dropped report. Only
the paths changed (wordcraft_doc is craft_wordmodel); the report helpers
are public so the format crates can call them; the image re-encoding
helper stays with WordCraft's RTF writer.
* Add craft-markdown: Markdown read and written, on craft-flow
WordCraft's crates/formats/src/markdown (07f97737) with its 35 tests,
moved onto craft-flow and craft-wordmodel. Character references come from
craft-html. The table cell budget lives in craft-flow, which names it in
its warning. Two test lines that wrote plain text through WordCraft's txt
module stay in WordCraft.
* WIP: craft-latex sources from WordCraft (unmodified)
* Add craft-latex: LaTeX documents read and written, on craft-flow
WordCraft's crates/formats/src/latex (07f97737) with its 21 tests, moved
onto craft-flow and craft-wordmodel; bytes are decoded by craft-mime's
detect. The LaTeX parts of WordCraft's cross-format tests (nesting,
column specs, labels, no TeX commands from the document, the fuzz and
round-trip properties) come along as tests/hostile.rs. The ligature test
builds its three-font paragraph as a flow instead of reading HTML.
* craft-math: latex_text, LaTeX math and the linear format converted as text
WordCraft's crates/formats/src/latex/math.rs (07f97737): the converter its
LaTeX document reader and writer use, which keeps the source's spacing.
It moves here so craft-math is the one place that converts LaTeX math;
craft-latex calls it, and its output does not change. Its tests (from
WordCraft's latex tests and cross-format tests) come along.
* craft-math: latex_text's group finders are public, for craft-latex's length parser
* craft-latex: math through craft_math::latex_text; label timing test by ratio
The document math converter moved to craft-math (#1040) unchanged; this
crate calls it there (and its length parser uses its group finders), so
export output is the same. The label test compared one read with a
ten-second wall clock and failed on a loaded machine; it now compares the
fastest of three reads of 25,000 and 100,000 labels, which a linear read
keeps near 4x and a quadratic one near 16x.
* Cargo.lock after the merge
* WIP: WordCraft's HTML document reader and writer, unmodified (crates/formats/src/html at 07f97737)
* craft-html: HTML documents on craft-flow, behind a flow feature
WordCraft's crates/formats/src/html (07f97737) as craft_html::doc: the
tag-soup builder, CSS subset, ruby and MathML reading, page decoding and
the writer, with its 44 tests. It reads tokens from this crate's
tokenizer through a small adapter (decoded text and attributes, names in
lower case) instead of its own lexer, and its character references are
this crate's. Lexer::set_pos is new: the ruby reader hands a token back.
The HTML parts of WordCraft's MathML and cross-format tests come along as
tests/doc.rs, the linear-time check as a ratio.
* craft-math: operator names take their LaTeX scripts directly again (restores #1019)
#1019 (22161d2d) wrote \sin^2\theta as \sin ^{2}{\theta } so it reads back as
the same tree. The squash merge of #1038, whose branch predated it, put
math_latex.rs and the corpus test back as they were before. This applies
#1019's change again, unchanged, with its test.
* Cargo.lock: craft-html's flow dependencies
---------
Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
394 lines
14 KiB
Rust
394 lines
14 KiB
Rust
use super::{Align, Document, FBlock, FTable, Flow, Fmt, Inline, Kind, Para, VAlign, base64_encode, mime_of, model};
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Export
|
|
|
|
/// Escape text for HTML content and attribute values.
|
|
pub fn esc(s: &str) -> String {
|
|
let mut o = String::with_capacity(s.len() + 8);
|
|
for c in s.chars() {
|
|
match c {
|
|
'&' => o.push_str("&"),
|
|
'<' => o.push_str("<"),
|
|
'>' => o.push_str(">"),
|
|
'"' => o.push_str("""),
|
|
'\u{000C}' | '\u{000E}' => {}
|
|
_ => o.push(c),
|
|
}
|
|
}
|
|
o
|
|
}
|
|
|
|
/// A font name as a quoted CSS string. Quotes, `\` and control characters would end the string,
|
|
/// and `;` and `,` the declaration or the family list for a reader that splits on them, so all of
|
|
/// these are written as hex escapes ([`css_unescape`] reads them back).
|
|
fn css_font_name(name: &str) -> String {
|
|
let mut o = String::with_capacity(name.len() + 2);
|
|
o.push('\'');
|
|
for c in name.chars() {
|
|
match c {
|
|
'\0' => o.push('\u{FFFD}'),
|
|
'\'' | '"' | '\\' | ';' | ',' => o.push_str(&format!("\\{:x} ", u32::from(c))),
|
|
c if c.is_control() => o.push_str(&format!("\\{:x} ", u32::from(c))),
|
|
c => o.push(c),
|
|
}
|
|
}
|
|
o.push('\'');
|
|
o
|
|
}
|
|
|
|
fn align_css(a: Option<Align>) -> Option<&'static str> {
|
|
match a? {
|
|
Align::Center => Some("text-align:center"),
|
|
Align::Right => Some("text-align:right"),
|
|
Align::Justify | Align::Distribute => Some("text-align:justify"),
|
|
Align::Left => None,
|
|
}
|
|
}
|
|
|
|
fn text_html(t: &str, at_start: bool) -> String {
|
|
let mut o = String::new();
|
|
let mut prev_space = at_start;
|
|
for c in t.chars() {
|
|
match c {
|
|
'\n' => {
|
|
o.push_str("<br>");
|
|
prev_space = true;
|
|
continue;
|
|
}
|
|
'\t' => o.push_str("<span style=\"white-space:pre\">\t</span>"),
|
|
' ' if prev_space => o.push_str(" "),
|
|
_ => o.push_str(&esc(&c.to_string())),
|
|
}
|
|
prev_space = c == ' ';
|
|
}
|
|
o
|
|
}
|
|
|
|
/// The tags that carry `f`'s character formatting: what opens them, and what closes them
|
|
/// (innermost last, so the closes are written in reverse).
|
|
fn fmt_tags(f: &Fmt) -> (String, Vec<&'static str>) {
|
|
let mut open = String::new();
|
|
let mut close: Vec<&str> = Vec::new();
|
|
// A script or other non-web target is written as plain text (`is_safe_href`).
|
|
if let Some(l) = f.link.as_deref().filter(|l| craft_wordmodel::link::is_safe_href(l)) {
|
|
open.push_str(&format!("<a href=\"{}\">", esc(l)));
|
|
close.push("</a>");
|
|
}
|
|
let mut tag = |o: &str, c: &'static str| {
|
|
open.push_str(o);
|
|
close.push(c);
|
|
};
|
|
if f.bold {
|
|
tag("<strong>", "</strong>");
|
|
}
|
|
if f.italic {
|
|
tag("<em>", "</em>");
|
|
}
|
|
if f.underline {
|
|
tag("<u>", "</u>");
|
|
}
|
|
if f.strike {
|
|
tag("<s>", "</s>");
|
|
}
|
|
if f.sup {
|
|
tag("<sup>", "</sup>");
|
|
} else if f.sub {
|
|
tag("<sub>", "</sub>");
|
|
}
|
|
if f.code {
|
|
tag("<code>", "</code>");
|
|
}
|
|
let mut css = Vec::new();
|
|
if let Some(c) = f.color {
|
|
css.push(format!("color:#{}", c.hex()));
|
|
}
|
|
if let Some(c) = f.background {
|
|
css.push(format!("background-color:#{}", c.hex()));
|
|
}
|
|
if let Some(s) = f.size {
|
|
css.push(format!("font-size:{}pt", trim_num(s)));
|
|
}
|
|
if let Some(fam) = &f.font {
|
|
css.push(format!("font-family:{}", esc(&css_font_name(fam))));
|
|
}
|
|
if !css.is_empty() {
|
|
open.push_str(&format!("<span style=\"{}\">", css.join(";")));
|
|
close.push("</span>");
|
|
}
|
|
(open, close)
|
|
}
|
|
|
|
/// Inline spans → HTML.
|
|
pub fn inlines_html(inlines: &[Inline]) -> String {
|
|
let mut o = String::new();
|
|
for i in inlines.iter() {
|
|
match i {
|
|
Inline::Text(t, f) => {
|
|
let (open, close) = fmt_tags(f);
|
|
let at_start = o.is_empty();
|
|
o.push_str(&open);
|
|
o.push_str(&text_html(t, at_start));
|
|
for c in close.iter().rev() {
|
|
o.push_str(c);
|
|
}
|
|
}
|
|
// A phonetic guide: the base text with its reading in `<rt>` (#1681). HTML ruby has
|
|
// no alignment or size, so the guide's own attributes stay in the document model.
|
|
Inline::Ruby { base, ruby, fmt } => {
|
|
let (open, close) = fmt_tags(fmt);
|
|
let at_start = o.is_empty();
|
|
o.push_str(&open);
|
|
o.push_str(&format!("<ruby>{}<rt>{}</rt></ruby>", text_html(base, at_start), text_html(ruby, false)));
|
|
for c in close.iter().rev() {
|
|
o.push_str(c);
|
|
}
|
|
}
|
|
Inline::Image(img) => {
|
|
o.push_str(&format!(
|
|
"<img src=\"data:{};base64,{}\" alt=\"{}\" width=\"{}\" height=\"{}\">",
|
|
mime_of(&img.ext),
|
|
base64_encode(&img.data),
|
|
esc(&img.alt),
|
|
(img.w / 0.75).round(),
|
|
(img.h / 0.75).round()
|
|
));
|
|
}
|
|
Inline::Anchor(a) => o.push_str(&format!("<a id=\"{}\"></a>", esc(a))),
|
|
Inline::Note { .. }
|
|
| Inline::Comment { .. }
|
|
| Inline::CommentEnd(_)
|
|
| Inline::Shape { .. }
|
|
| Inline::Group { .. }
|
|
| Inline::FieldStart(_)
|
|
| Inline::FieldEnd
|
|
| Inline::AnchorEnd(_) => {}
|
|
// ASK and SET show nothing (see `model::shows_nothing`).
|
|
Inline::Field { instr, .. } if model::shows_nothing(instr) => {}
|
|
Inline::Field { result, fmt, .. } | Inline::Control { text: result, fmt, .. } => {
|
|
let (open, close) = fmt_tags(fmt);
|
|
let at_start = o.is_empty();
|
|
o.push_str(&open);
|
|
o.push_str(&text_html(result, at_start));
|
|
for c in close.iter().rev() {
|
|
o.push_str(c);
|
|
}
|
|
}
|
|
// No picture to show: the browser shows the alt text in its place.
|
|
Inline::Figure(alt) | Inline::Chart(craft_flow::Chart { alt, .. }) => o.push_str(&format!("<img alt=\"{}\">", esc(alt))),
|
|
// MathML, which browsers, Word and LibreOffice show as an equation (#816).
|
|
Inline::Equation { linear, display } => o.push_str(&craft_math::mathml::linear_to_mathml(linear, *display)),
|
|
}
|
|
}
|
|
o
|
|
}
|
|
|
|
fn trim_num(v: f32) -> String {
|
|
let s = format!("{:.2}", v);
|
|
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
|
}
|
|
|
|
fn para_attrs(p: &Para) -> String {
|
|
let mut css: Vec<&str> = Vec::new();
|
|
if let Some(a) = align_css(p.align) {
|
|
css.push(a);
|
|
}
|
|
if p.page_break {
|
|
css.push("break-before:page;page-break-before:always");
|
|
}
|
|
if css.is_empty() { String::new() } else { format!(" style=\"{}\"", css.join(";")) }
|
|
}
|
|
|
|
fn para_html(p: &Para) -> String {
|
|
let body = inlines_html(&p.inlines);
|
|
let attrs = para_attrs(p);
|
|
match p.kind {
|
|
Kind::Heading(n) => format!("<h{n}{attrs}>{body}</h{n}>", n = n.clamp(1, 6)),
|
|
Kind::Title => format!("<h1{attrs}>{body}</h1>"),
|
|
Kind::Rule => "<hr>".to_string(),
|
|
_ if body.is_empty() => format!("<p{attrs}> </p>"),
|
|
_ => format!("<p{attrs}>{body}</p>"),
|
|
}
|
|
}
|
|
|
|
fn table_html(t: &FTable, out: &mut String, depth: usize) {
|
|
out.push_str("<table style=\"border-collapse:collapse\">\n");
|
|
for row in &t.rows {
|
|
out.push_str("<tr>");
|
|
for c in row {
|
|
if c.covered {
|
|
continue;
|
|
}
|
|
let tag = if c.header { "th" } else { "td" };
|
|
let mut attrs = String::new();
|
|
if c.colspan > 1 {
|
|
attrs.push_str(&format!(" colspan=\"{}\"", c.colspan));
|
|
}
|
|
if c.rowspan > 1 {
|
|
attrs.push_str(&format!(" rowspan=\"{}\"", c.rowspan));
|
|
}
|
|
let v_css = match c.valign {
|
|
Some(VAlign::Center) => "vertical-align:middle",
|
|
Some(VAlign::Bottom) => "vertical-align:bottom",
|
|
_ => "vertical-align:top",
|
|
};
|
|
let mut css = vec!["padding:4px 6px".to_string(), v_css.to_string()];
|
|
if !t.borderless {
|
|
css.insert(0, "border:1px solid #999".to_string());
|
|
}
|
|
if let Some(s) = c.shading {
|
|
css.push(format!("background-color:#{}", s.hex()));
|
|
}
|
|
let single = match c.blocks.as_slice() {
|
|
[FBlock::Para(p)] if p.kind == Kind::Normal && p.list.is_none() && !p.page_break => Some(p),
|
|
_ => None,
|
|
};
|
|
if let Some(a) = single.and_then(|p| align_css(p.align)) {
|
|
css.push(a.to_string());
|
|
}
|
|
out.push_str(&format!("<{tag}{attrs} style=\"{}\">", css.join(";")));
|
|
match single {
|
|
Some(p) => out.push_str(&inlines_html(&p.inlines)),
|
|
None => {
|
|
out.push('\n');
|
|
blocks_html(&c.blocks, out, depth + 1);
|
|
}
|
|
}
|
|
out.push_str(&format!("</{tag}>"));
|
|
}
|
|
out.push_str("</tr>\n");
|
|
}
|
|
out.push_str("</table>\n");
|
|
}
|
|
|
|
fn blocks_html(blocks: &[FBlock], out: &mut String, depth: usize) {
|
|
let mut i = 0;
|
|
while let Some(b) = blocks.get(i) {
|
|
match b {
|
|
FBlock::Table(t) => {
|
|
if depth < model::MAX_DEPTH {
|
|
table_html(t, out, depth);
|
|
}
|
|
i += 1;
|
|
}
|
|
FBlock::Para(p) if p.list.is_some() => {
|
|
// Nested lists from consecutive list paragraphs.
|
|
let mut stack: Vec<(bool, bool)> = Vec::new(); // (ordered, li open)
|
|
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
|
let Some(li) = q.list else { break };
|
|
let lv = li.level.min(8) as usize;
|
|
while stack.len() > lv + 1 {
|
|
close_list(&mut stack, out);
|
|
}
|
|
if stack.len() == lv + 1 && stack.last().is_some_and(|s| s.0 != li.ordered) {
|
|
close_list(&mut stack, out);
|
|
}
|
|
while stack.len() < lv + 1 {
|
|
if let Some(top) = stack.last_mut()
|
|
&& !top.1
|
|
{
|
|
out.push_str("<li>");
|
|
top.1 = true;
|
|
}
|
|
out.push_str(if li.ordered { "<ol>" } else { "<ul>" });
|
|
stack.push((li.ordered, false));
|
|
}
|
|
if let Some(top) = stack.last_mut() {
|
|
if top.1 {
|
|
out.push_str("</li>\n");
|
|
}
|
|
top.1 = true;
|
|
}
|
|
let body = inlines_html(&q.inlines);
|
|
let attrs = para_attrs(q);
|
|
out.push_str(&format!("<li{attrs}>{body}"));
|
|
i += 1;
|
|
}
|
|
while !stack.is_empty() {
|
|
close_list(&mut stack, out);
|
|
}
|
|
out.push('\n');
|
|
}
|
|
FBlock::Para(p) if p.kind == Kind::Code => {
|
|
out.push_str("<pre><code>");
|
|
let mut first = true;
|
|
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
|
if q.kind != Kind::Code || q.list.is_some() {
|
|
break;
|
|
}
|
|
if !first {
|
|
out.push('\n');
|
|
}
|
|
first = false;
|
|
out.push_str(&esc(&q.text()));
|
|
i += 1;
|
|
}
|
|
out.push_str("</code></pre>\n");
|
|
}
|
|
FBlock::Para(p) if p.kind == Kind::Quote => {
|
|
out.push_str("<blockquote>\n");
|
|
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
|
if q.kind != Kind::Quote || q.list.is_some() {
|
|
break;
|
|
}
|
|
out.push_str(¶_html(&Para { kind: Kind::Normal, ..q.clone() }));
|
|
out.push('\n');
|
|
i += 1;
|
|
}
|
|
out.push_str("</blockquote>\n");
|
|
}
|
|
FBlock::Para(p) => {
|
|
out.push_str(¶_html(p));
|
|
out.push('\n');
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn close_list(stack: &mut Vec<(bool, bool)>, out: &mut String) {
|
|
if let Some((ordered, li_open)) = stack.pop() {
|
|
if li_open {
|
|
out.push_str("</li>");
|
|
}
|
|
out.push_str(if ordered { "</ol>" } else { "</ul>" });
|
|
}
|
|
}
|
|
|
|
/// A complete HTML document.
|
|
pub fn export(doc: &Document) -> String {
|
|
let flow = model::from_doc(doc);
|
|
export_flow(&flow, doc.styles.default_chr.lang.as_deref().unwrap_or("en"))
|
|
}
|
|
|
|
/// `doc`'s content as an HTML fragment (no head or style sheet): what a copy puts on the system
|
|
/// clipboard beside its text, equations as MathML (#816).
|
|
pub fn export_fragment(doc: &Document) -> String {
|
|
let flow = model::from_doc(doc);
|
|
let mut out = String::from("<meta charset=\"utf-8\">");
|
|
blocks_html(&flow.blocks, &mut out, 0);
|
|
out
|
|
}
|
|
|
|
pub fn export_flow(flow: &Flow, lang: &str) -> String {
|
|
let m = &flow.meta;
|
|
let mut out = String::new();
|
|
out.push_str("<!DOCTYPE html>\n");
|
|
out.push_str(&format!("<html lang=\"{}\">\n<head>\n<meta charset=\"utf-8\">\n", esc(lang)));
|
|
out.push_str("<meta name=\"generator\" content=\"WordCraft\">\n");
|
|
out.push_str(&format!("<title>{}</title>\n", esc(&m.title)));
|
|
for (k, v) in [("author", &m.author), ("description", &m.description), ("keywords", &m.keywords), ("subject", &m.subject)] {
|
|
if !v.is_empty() {
|
|
out.push_str(&format!("<meta name=\"{k}\" content=\"{}\">\n", esc(v)));
|
|
}
|
|
}
|
|
out.push_str(
|
|
"<style>\nbody{font-family:Aptos,Calibri,'Segoe UI',Arial,sans-serif;font-size:12pt;line-height:1.15;max-width:48em;margin:2em auto;padding:0 1em}\n\
|
|
table{border-collapse:collapse;margin:0.5em 0}\nblockquote{margin:0.5em 2em;font-style:italic;color:#404040}\n\
|
|
pre{background:#f5f5f5;padding:0.5em;font-family:'Courier New',monospace;font-size:10pt}\n</style>\n</head>\n<body>\n",
|
|
);
|
|
blocks_html(&flow.blocks, &mut out, 0);
|
|
out.push_str("</body>\n</html>\n");
|
|
out
|
|
}
|