///|
// Prawn's formatted text (`formatted_text`, `text` with `inline_format`):
// fragments with their own styles, and the inline markup Prawn's
// `Text::Formatted::Parser` reads.

///|
/// A fragment of formatted text (Prawn's fragment hash). Unset fields take
/// the document's (font, size, fill colour).
pub(all) struct TextFragment {
  text : String
  bold : Bool
  italic : Bool
  underline : Bool
  strikethrough : Bool
  subscript : Bool
  superscript : Bool
  font : String?
  size : Double?
  /// six hex digits (`"FF0000"`)
  color : String?
  /// a CMYK colour (0 to 100 each), not set yet
  cmyk : Array[Double]?
  /// extra space between characters, not set yet
  character_spacing : Double?
  /// a URI the text links to
  link : String?
  /// a named destination the text links to
  anchor : String?
  /// called as each piece of the fragment is drawn (Prawn's `callback`)
  callbacks : Array[FragmentCallback]
}

///|
/// A piece of a fragment as it is drawn (Prawn's fragment, as callbacks
/// see it), in the coordinates of the bounds: its left side, baseline,
/// width, and its font's top (baseline plus ascender) and bottom
/// (baseline less descender).
pub(all) struct PlacedFragment {
  text : String
  left : Double
  baseline : Double
  width : Double
  height : Double
  top : Double
  bottom : Double
}

///|
/// A fragment callback (Prawn's callback object): `render_behind` before
/// the text is drawn, `render_in_front` after it and its decorations and
/// links. Not called in dry runs (measuring, shrinking, aligning).
pub(all) struct FragmentCallback {
  render_behind : ((PlacedFragment) -> Unit raise PrawnError)?
  render_in_front : ((PlacedFragment) -> Unit raise PrawnError)?
}

///|
/// A plain fragment.
pub fn TextFragment::new(text : String) -> TextFragment {
  {
    text,
    bold: false,
    italic: false,
    underline: false,
    strikethrough: false,
    subscript: false,
    superscript: false,
    font: None,
    size: None,
    color: None,
    cmyk: None,
    character_spacing: None,
    link: None,
    anchor: None,
    callbacks: [],
  }
}

///|
/// The tags Prawn's parser knows exactly (`PARSER_REGEX`), in its order.
let exact_tags : Array[String] = [
  "", "", "", "", "", "", "", "",
  "", "", "", "",
]

///|
/// The tag at `i` of `s` (``, ``, ``, `` taking
/// anything up to the next `>`), as Prawn's regex alternation finds it.
fn tag_at(s : String, i : Int) -> String? {
  let rest = s[i:].to_owned()
  let open = fn(name : String) -> String? {
    guard rest.has_prefix(name) else { return None }
    match rest.find(">") {
      Some(end) => Some(rest[:end + 1].to_owned())
      None => None
    }
  }
  for t in exact_tags {
    if rest.has_prefix(t) {
      return Some(t)
    }
  }
  match open(" return Some(t)
    None => ()
  }
  if rest.has_prefix("") {
    return Some("")
  }
  match open(" return Some(t)
    None => ()
  }
  if rest.has_prefix("") {
    return Some("")
  }
  match open(" return Some(t)
    None => ()
  }
  if rest.has_prefix("") {
    return Some("")
  }
  for t in ["", "", "", ""] {
    if rest.has_prefix(t) {
      return Some(t)
    }
  }
  match open(" return Some(t)
    None => ()
  }
  if rest.has_prefix("") {
    return Some("")
  }
  None
}

///|
/// An attribute of a tag, quoted with `"` or `'` (`name="value"`); with
/// `hash`, an optional `#` before the value is dropped.
fn attribute(tag : String, name : String, hash? : Bool = false) -> String? {
  for quote in ["\"", "'"] {
    let start = name + "=" + quote
    match tag.find(start) {
      Some(i) => {
        let mut from = i + start.length()
        if hash && from < tag.length() && tag[from] == '#' {
          from = from + 1
        }
        match tag[from:].find(quote) {
          Some(end) => return Some(tag[from:from + end].to_owned())
          None => ()
        }
      }
      None => ()
    }
  }
  None
}

///|
fn unescape(text : String) -> String {
  text
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
  .replace_all(old="&", new="&")
}

///|
/// Prawn's inline format (`Text::Formatted::Parser.format`): ``/
/// ``, ``/``, ``, ``, ``, ``,
/// ``, ``, ``/``, `
`, /// and `<`, `>`, `&`. A `<` that starts no tag is dropped, as /// Prawn's scan skips it. pub fn parse_inline_format( string : String, ) -> Array[TextFragment] raise PrawnError { //
,
and
are line breaks let s = StringBuilder() let mut i = 0 while i < string.length() { if string[i:].has_prefix("' { s.write_char('\n') i = j + 1 continue } } s.write_char(string[i].unsafe_to_char()) i = i + 1 } let text = s.to_string() let out : Array[TextFragment] = [] let styles : Array[String] = [] let colors : Array[(String?, Array[Double]?)] = [] let fonts : Array[String] = [] let sizes : Array[Double] = [] let spacings : Array[Double] = [] let mut link : String? = None let mut anchor : String? = None // every occurrence, as Ruby's Array#delete removes them let remove = fn(style : String) { styles.retain(s => s != style) } let mut i = 0 while i < text.length() { let c = text[i] if c == '\n' { out.push( fragment_of( "\n", styles, colors.last(), fonts.last(), sizes.last(), spacings.last(), link, anchor, ), ) i = i + 1 continue } if c != '<' { let mut j = i while j < text.length() && text[j] != '<' && text[j] != '\n' { j = j + 1 } out.push( fragment_of( unescape(text[i:j].to_owned()), styles, colors.last(), fonts.last(), sizes.last(), spacings.last(), link, anchor, ), ) i = j continue } guard tag_at(text, i) is Some(tag) else { // a `<` that starts no tag i = i + 1 continue } i = i + tag.length() match tag { "" | "" => styles.push("bold") "" | "" => styles.push("italic") "" => styles.push("underline") "" => styles.push("strikethrough") "" => styles.push("subscript") "" => styles.push("superscript") "" | "" => remove("bold") "" | "" => remove("italic") "" => remove("underline") "
" => remove("strikethrough") "" => remove("subscript") "" => remove("superscript") "" | "" => { link = None anchor = None } "" => colors.pop() |> ignore // pops what may not have pushed, as Prawn does "" => { fonts.pop() |> ignore sizes.pop() |> ignore spacings.pop() |> ignore } // an attribute left out keeps what an outer tag set _ if tag.has_prefix(" { match attribute(tag, "href") { Some(h) => link = Some(h) None => () } match attribute(tag, "anchor") { Some(a) => anchor = Some(a) None => () } } _ if tag.has_prefix(" { match attribute(tag, "rgb", hash=true) { Some(rgb) => colors.push((Some(rgb), None)) None => () } // c, m, y and k all given: a CMYK colour let parts = ["c", "m", "y", "k"].map(n => attribute(tag, n, hash=true)) if parts.iter().all(p => p is Some(_)) { let values = [] for p in parts { values.push( @string.parse_int(p.unwrap()).to_double() catch { _ => raise ArgumentError( "invalid value for Integer(): \{p.unwrap()}", ) }, ) } colors.push((None, Some(values))) } } _ if tag.has_prefix(" { match attribute(tag, "name") { Some(name) => fonts.push(name) None => () } // Ruby's Float(): a bad number is an ArgumentError match attribute(tag, "size") { Some(size) => sizes.push(ruby_float(size)) None => () } match attribute(tag, "character_spacing") { Some(spacing) => spacings.push(ruby_float(spacing)) None => () } } _ => () } } out } ///| fn fragment_of( text : String, styles : Array[String], color : (String?, Array[Double]?)?, font : String?, size : Double?, spacing : Double?, link : String?, anchor : String?, ) -> TextFragment { { text, bold: styles.contains("bold"), italic: styles.contains("italic"), underline: styles.contains("underline"), strikethrough: styles.contains("strikethrough"), subscript: styles.contains("subscript"), superscript: styles.contains("superscript"), font, size, color: color.bind(c => c.0), cmyk: color.bind(c => c.1), character_spacing: spacing, link, anchor, callbacks: [], } } ///| /// A fragment as it is set: its font, size (a sub- or superscript at /// 0.583 of it, as Prawn's arranger sets them), colour, styles and link. ///| /// What Prawn knows of a fragment that its style does not carry: whether /// it sets its own colour (Prawn then strokes its decorations in it), and /// the destination it links to besides a URI. priv struct FragmentMeta { color_set : Bool /// the font it is set in, by name and style (the current font while it /// is drawn), and the styles it asks for itself (which its fallback /// fonts are found in) font_name : String font_flags : (Bool, Bool) styles : (Bool, Bool) /// the font it names itself (Prawn's fragment `:font`) font : String? /// whether the font the fragment is set in has a family (None: it keeps /// the current font, setting only the size) family_known : Bool? anchor : String? callbacks : Array[FragmentCallback] } ///| /// A standard font's face named directly (`Times-Bold`): its family and /// whether it is bold and italic. fn standard_face(name : String) -> (String, Bool, Bool)? { // a family name (Courier, Helvetica) is the family, styles and all if standard_families.iter().any(entry => entry.0 == name) { return None } for entry in standard_families { for i, face in entry.1 { if face == name { return Some((entry.0, i == 1 || i == 3, i == 2 || i == 3)) } } } None } ///| fn Document::fragment( self : Document, f : TextFragment, size : Double?, ) -> Fragment raise PrawnError { if f.cmyk is Some(_) { raise Unsupported("CMYK colour") } if f.character_spacing is Some(_) { raise Unsupported("character spacing") } let base = self.style(size?) // a face named directly is that face, whatever the styles say // styles need a family to take them from (a font of its own too: Prawn's // arranger raises as it sets the fragment, before anything of the text is drawn here) if (f.font is Some(_) || f.bold || f.italic) && !self.font_family_known { raise BadFontFamily("Prawn::Text::Formatted::Arranger::BadFontFamily") } // the font it is set in, by name and style let (name, flags, font_set) = match f.font { Some(name) => match standard_face(name) { Some((family, bold, italic)) => (family, (bold, italic), true) // a font file of its own is its only face None if is_font_file(name) => (name, (false, false), true) None => (name, (f.bold, f.italic), true) } // no style of its own: the current font, as Prawn's arranger then sets // only the size None if !f.bold && !f.italic => (self.family, self.face_style, false) None => (self.family, (f.bold, f.italic), false) } // the catalog's face of it: the current font's as it was set, when the // fragment keeps it; else read as needed (a TrueType family's face or a // file) let (family, bold, italic) = if f.font is None && !f.bold && !f.italic { self.font_key } else { self.ensure_font(name, flags) self.catalog_key(name, flags) } let meta = self.fragment_meta.length() self.fragment_meta.push({ color_set: f.color is Some(_), font_name: name, font_flags: flags, styles: (f.bold, f.italic), font: f.font, family_known: match f.font { // a face by its own name, or a file: a font of no family Some(name) => Some(standard_face(name) is None && !is_font_file(name)) None if f.bold || f.italic => Some(true) None => None }, anchor: if f.link is Some(_) { f.anchor } else { None }, callbacks: f.callbacks, }) let mut size = f.size.unwrap_or(base.size) if f.subscript || f.superscript { size = size * 0.583 } let color = match f.color { Some(hex) => color_of_hex(hex) None => base.color } { text: f.text, style: { ..base, family, size, bold, italic, font_set, callback: meta, color, underline: f.underline, strike: f.strikethrough, // a subscript first, as Prawn's `y_offset` checks script: if f.subscript { -1 } else if f.superscript { 1 } else { 0 }, link: match (f.link, f.anchor) { (Some(uri), _) => Some(Uri(uri)) (None, Some(name)) => Some(Named(name)) _ => None }, }, anchor: None, } } ///| /// Prawn's `formatted_text`: `fragments` flowed from the cursor as `text` /// flows a string. pub fn Document::formatted_text( self : Document, fragments : Array[TextFragment], size? : Double, style? : FontStyle, align? : Alignment = Left, leading? : Double, draw_text_callback? : (String, (Double, Double), Bool) -> Unit raise PrawnError, ) -> Unit raise PrawnError { // the current font in `style` while this is set (Prawn's `:style`, which // its boxes apply and undo) let saved_font = self.apply_style( style, BadFontFamily("PDF::Core::Text::BadFontFamily"), ) defer self.restore_style(saved_font) // the pieces drawn by the callback instead (text, where, kerning) let saved_callback = self.draw_text_callback self.draw_text_callback = draw_text_callback defer { self.draw_text_callback = saved_callback } let pieces = fragments.filter(f => !f.text.is_empty()) if pieces.is_empty() { self.move_down(leading.unwrap_or(self.default_leading)) return } let set = pieces.map(f => self.fragment(f, size)) // every fragment's encoding first, as Prawn normalizes them all before // wrapping for piece in set { self.check(piece.text, piece.style, None) } for piece in set { self.check(piece.text, piece.style, Some(self.bbox.side().1)) } let base = self.style(size?) // Prawn's `formatted_text`: text boxes, each as tall as the room left, // the next column or page between them, until the text is set (or a // fresh one takes none of it) let mut remaining = self.fill_box(set, base, align, leading) while !remaining.is_empty() { self.move_past_bottom() let before = remaining remaining = self.fill_box(remaining, base, align, leading) // Prawn compares the fragments, empty ones too if remaining.length() == before.length() && remaining.iter().zip(before.iter()).all(p => p.0.text == p.1.text) { break } } } ///| /// Prawn's `fill_formatted_text_box`: as much of `fragments` as fits in a /// box from the position down to the bottom of the bounds (of the margin /// box, in a stretchy box), across the bounds (a column box's column); /// then the position moves down by what was set and the line gap and /// leading (Prawn's final gap), from wherever callbacks left it. The rest /// is returned. fn Document::fill_box( self : Document, fragments : Array[Fragment], base : Style, align : Alignment, leading : Double?, ) -> Array[Fragment] raise PrawnError { // the document's default leading unless given, read as each box is made // (Prawn's Box, which a callback changing it affects) let leading = leading.unwrap_or(self.default_leading) let box = self.bbox let y = self.y() let bottom = if box.height is None { self.margin.bottom_at(y) } else { box.bottom_at(y) } let (side, width) = box.side() let (rest, printed, gap) = self.set_box( fragments, base, (side, y), width, y - bottom, align, leading, ) self.set_y(self.y() - printed - (gap + leading)) rest } ///| /// Ruby's `Float()` of a string: an optional sign, then digits (with /// single underscores between them) with an optional fraction (`.5` too) /// and exponent, or a hex integer (`0x10`), with surrounding whitespace; /// anything else is an ArgumentError. fn ruby_float(s : String) -> Double raise PrawnError { let bad = ArgumentError("invalid value for Float(): \"\{s}\"") let t = s.trim(chars=" \t\n\r\u{0B}\u{0C}").to_owned() // hex: [sign] 0x digits let unsigned = if t.has_prefix("+") || t.has_prefix("-") { t[1:].to_owned() } else { t } if unsigned.has_prefix("0x") || unsigned.has_prefix("0X") { let hex = unsigned[2:].to_owned() if hex.is_empty() || hex.has_prefix("_") || hex.has_suffix("_") || hex.contains("__") { raise bad } let mut value = 0.0 for c in hex { let digit = match c { '0'..='9' => c.to_int() - '0'.to_int() 'a'..='f' => c.to_int() - 'a'.to_int() + 10 'A'..='F' => c.to_int() - 'A'.to_int() + 10 '_' => continue _ => raise bad } value = value * 16.0 + digit.to_double() } return if t.has_prefix("-") { -value } else { value } } let digits = fn(from : Int) -> Int { // how far a run of digits with single underscores between them goes let mut i = from while i < t.length() && ( t[i].unsafe_to_char().is_ascii_digit() || ( t[i] == '_' && i > from && i + 1 < t.length() && t[i + 1].unsafe_to_char().is_ascii_digit() ) ) { i = i + 1 } i } let mut i = 0 if i < t.length() && (t[i] == '+' || t[i] == '-') { i = i + 1 } let start = i let int_end = digits(start) // no integer part only before a fraction (`.5`) if int_end == start && !(start < t.length() && t[start] == '.') { raise bad } i = int_end if i < t.length() && t[i] == '.' { let frac_end = digits(i + 1) // `1.` is 1.0, `.` alone is nothing if frac_end == i + 1 && int_end == start { raise bad } i = frac_end } if i < t.length() && (t[i] == 'e' || t[i] == 'E') { let mut j = i + 1 if j < t.length() && (t[j] == '+' || t[j] == '-') { j = j + 1 } let exp_end = digits(j) if exp_end == j { raise bad } i = exp_end } if i != t.length() { raise bad } // in a form parse_double takes: digits on both sides of the point let n = t .replace_all(old="_", new="") .replace_all(old=".e", new=".0e") .replace_all(old=".E", new=".0E") let n = if n.has_suffix(".") { n + "0" } else { n } let n = if n.has_prefix(".") { "0" + n } else if n.has_prefix("+.") || n.has_prefix("-.") { n[:1].to_owned() + "0" + n[1:].to_owned() } else { n } @string.parse_double(n) catch { _ => raise bad } } ///| /// A fragment as Prawn's callbacks see it: `left` and `baseline` in the /// coordinates of the bounds, the baseline raised to a sub- or /// superscript's. fn placed_fragment( left : Double, width : Double, baseline : Double, style : Style, face : Face, text : String, ) -> PlacedFragment { let a = face.ascender_pt(style.size) let d = face.descender_pt(style.size) let base = baseline + script_rise(style, face) { text, left, baseline: base, width, height: a + d, top: base + a, bottom: base - d, } } ///| /// The document's state around a fragment being drawn (Prawn's arranger): /// what to put back when the fragment is done. priv struct FragmentScope { family : String size : Double face_style : (Bool, Bool) family_known : Bool font_key : (String, Bool, Bool) fill : @pagelayout.Color stroke : @pagelayout.Color color_set : Bool } ///| /// A run's fragment starts being drawn: the document takes its font and /// size, and its own colour as fill and stroke colour, for the underlay, /// text, decorations and callbacks that follow. fn Document::begin_fragment(self : Document, style : Style) -> Unit { let gs = self.gs() let color_set = self.fragment_meta.get(style.callback) is Some({ color_set: true, .. }) self.scopes.push({ family: self.family, size: self.size, face_style: self.face_style, family_known: self.font_family_known, font_key: self.font_key, fill: gs.fill, stroke: gs.stroke, color_set, }) // the catalog's face it is set in self.font_key = (style.family, style.bold, style.italic) self.size = style.size match self.fragment_meta.get(style.callback) { Some(meta) => { self.family = meta.font_name self.face_style = meta.font_flags } // text of no record (a text box's) is in the current font, which it // keeps by family and style None => () } match self.fragment_meta.get(style.callback) { Some({ family_known: Some(known), .. }) => self.font_family_known = known _ => () } if color_set { gs.fill = style.color gs.stroke = style.color } } ///| /// The fragment is drawn: font and size back, and a fragment's own colour /// taken off again on the page current now (a colour a callback set in a /// fragment without one stays, as with Prawn's arranger). fn Document::end_fragment(self : Document) -> Unit { guard self.scopes.pop() is Some(scope) else { return } self.family = scope.family self.size = scope.size self.face_style = scope.face_style self.font_family_known = scope.family_known self.font_key = scope.font_key if scope.color_set { let gs = self.gs() gs.fill = scope.fill gs.stroke = scope.stroke } }