///|
/// Splits a string into lines, trimming trailing whitespace (or only the
/// line ending when `trim_end` is false). Ruby `Helpers.prepare_source_string`.
pub fn prepare_source_string(
data : String,
trim_end? : Bool = true,
) -> Array[String] {
if data == "" {
return []
}
let data = if data.has_prefix("\u{FEFF}") { @rb.from(data, 1) } else { data }
let lines = []
let n = data.length()
let mut start = 0
let mut i = 0
while i < n {
if data[i] == '\n' {
let line = data.unsafe_substring(start~, end=i + 1)
lines.push(if trim_end { @rb.rstrip(line) } else { @rb.chomp(line) })
start = i + 1
}
i += 1
}
if start < n {
let line = data.unsafe_substring(start~, end=n)
lines.push(if trim_end { @rb.rstrip(line) } else { @rb.chomp(line) })
}
lines
}
///|
/// Ruby `Helpers.prepare_source_array`.
pub fn prepare_source_array(
data : Array[String],
trim_end? : Bool = true,
) -> Array[String] {
if data.is_empty() {
return []
}
let out = data.map(line => {
if trim_end {
@rb.rstrip(line)
} else {
@rb.chomp(line)
}
})
if out[0].has_prefix("\u{FEFF}") {
out[0] = @rb.from(out[0], 1)
}
out
}
///|
/// Whether `str` looks like a URI (Ruby `Helpers.uriish?`).
pub fn is_uriish(str : String) -> Bool {
str.contains(":") && uri_sniff_rx.matches(str)
}
///|
/// Percent-encodes a URI component (Ruby `CGI.escapeURIComponent`).
pub fn encode_uri_component(str : String) -> String {
let sb = StringBuilder()
let hex = "0123456789ABCDEF"
let bytes = @utf8.encode(str)
for b in bytes {
let c = b.to_int()
if (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '.' ||
c == '_' ||
c == '~' {
sb.write_char(c.unsafe_to_char())
} else {
sb.write_char('%')
sb.write_char(hex[c >> 4].to_int().unsafe_to_char())
sb.write_char(hex[c & 15].to_int().unsafe_to_char())
}
}
sb.to_string()
}
///|
/// Replaces spaces with `%20`.
pub fn encode_spaces_in_uri(str : String) -> String {
if str.contains(" ") {
str.replace_all(old=" ", new="%20")
} else {
str
}
}
///|
/// Removes the file extension (Ruby `Helpers.rootname`).
pub fn rootname(filename : String) -> String {
match filename.rev_find(".") {
Some(i) =>
if @rb.index_of(filename, "/", from=i) is Some(_) {
filename
} else {
@rb.slice(filename, 0, i)
}
None => filename
}
}
///|
/// Last path segment, optionally without extension (Ruby `Helpers.basename`).
pub fn basename(filename : String, drop_ext? : Bool = false) -> String {
// File.basename strips all trailing slashes; a path of only slashes is "/"
let mut trimmed = filename
while trimmed.length() > 1 && trimmed.has_suffix("/") {
trimmed = @rb.chop(trimmed)
}
if trimmed == "/" {
return trimmed
}
let base = match trimmed.rev_find("/") {
Some(i) => @rb.from(trimmed, i + 1)
None => trimmed
}
if drop_ext {
let ext = extname(base, fallback="")
if ext != "" && ext != base {
@rb.slice(base, 0, base.length() - ext.length())
} else {
base
}
} else {
base
}
}
///|
/// Directory part of a path (Ruby `File.dirname`).
pub fn dirname(path : String) -> String {
let p = if path.length() > 1 && path.has_suffix("/") {
@rb.chop(path)
} else {
path
}
match p.rev_find("/") {
Some(0) => "/"
Some(i) => @rb.slice(p, 0, i)
None => "."
}
}
///|
/// Whether the path has a file extension.
pub fn has_extname(path : String) -> Bool {
match path.rev_find(".") {
Some(i) => @rb.index_of(path, "/", from=i) is None
None => false
}
}
///|
/// The file extension including the dot, or `fallback`.
pub fn extname(path : String, fallback? : String = "") -> String {
match path.rev_find(".") {
Some(i) =>
if @rb.index_of(path, "/", from=i) is Some(_) {
fallback
} else {
@rb.from(path, i)
}
None => fallback
}
}
///|
/// Ruby `Helpers.roman_to_int`.
pub fn roman_to_int(val : String) -> Int {
let value = fn(c : Char) -> Int {
match c {
'I' => 1
'V' => 5
'X' => 10
'L' => 50
'C' => 100
'D' => 500
'M' => 1000
_ => 0
}
}
let vals = val.to_array().map(value)
let mut result = 0
for i, v in vals {
if i + 1 < vals.length() && vals[i + 1] > v {
result -= v
} else {
result += v
}
}
result
}
///|
/// Joins a path onto a base (Ruby `File.join`).
fn file_join(a : String, b : String) -> String {
if a == "" {
b
} else if a.has_suffix("/") {
a + b
} else {
a + "/" + b
}
}
///|
/// Expands `path` against `cwd` (Ruby `File.expand_path`, no symlinks).
pub fn expand_path_from(cwd : String, path : String) -> String {
let full = if path.has_prefix("/") { path } else { file_join(cwd, path) }
let segs = []
for s in @rb.split(full, "/") {
if s == "" || s == "." {
continue
} else if s == ".." {
segs.pop() |> ignore
} else {
segs.push(s)
}
}
"/" + segs.join("/")
}
///|
/// Base64 encodes bytes (Ruby `[data].pack 'm0'`).
pub fn base64_encode(data : Bytes) -> String {
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let sb = StringBuilder()
let n = data.length()
let mut i = 0
while i + 2 < n {
let v = (data[i].to_int() << 16) |
(data[i + 1].to_int() << 8) |
data[i + 2].to_int()
sb.write_char(chars[(v >> 18) & 63].to_int().unsafe_to_char())
sb.write_char(chars[(v >> 12) & 63].to_int().unsafe_to_char())
sb.write_char(chars[(v >> 6) & 63].to_int().unsafe_to_char())
sb.write_char(chars[v & 63].to_int().unsafe_to_char())
i += 3
}
if n - i == 1 {
let v = data[i].to_int() << 16
sb.write_char(chars[(v >> 18) & 63].to_int().unsafe_to_char())
sb.write_char(chars[(v >> 12) & 63].to_int().unsafe_to_char())
sb.write_string("==")
} else if n - i == 2 {
let v = (data[i].to_int() << 16) | (data[i + 1].to_int() << 8)
sb.write_char(chars[(v >> 18) & 63].to_int().unsafe_to_char())
sb.write_char(chars[(v >> 12) & 63].to_int().unsafe_to_char())
sb.write_char(chars[(v >> 6) & 63].to_int().unsafe_to_char())
sb.write_char('=')
}
sb.to_string()
}
// ---------------------------------------------------------------------------
// AbstractNode path / asset helpers
///|
/// Ruby `icon_uri(name)`.
pub fn Node::icon_uri(self : Node, name : String) -> String {
let doc = self.document()
let icon = if self.has_attr("icon") {
let icon = self.attr("icon").unwrap_or("")
if has_extname(icon) {
icon
} else {
"\{icon}.\{doc.attr("icontype", default="png").unwrap_or("png")}"
}
} else {
"\{name}.\{doc.attr("icontype", default="png").unwrap_or("png")}"
}
self.image_uri(icon, asset_dir_key=Some("iconsdir"))
}
///|
/// Ruby `image_uri(target, asset_dir_key)`.
pub fn Node::image_uri(
self : Node,
target_image : String,
asset_dir_key? : String? = Some("imagesdir"),
) -> String {
let doc = self.document()
let images_base = match asset_dir_key {
Some(k) => self.attr(k, inherited=true)
None => None
}
if doc.safe() < SAFE_SECURE && doc.has_attr("data-uri") {
if is_uriish(target_image) {
let t = encode_spaces_in_uri(target_image)
return t
}
match images_base {
Some(base) if is_uriish(base) =>
return self.normalize_web_path(
target_image,
start=base,
preserve_uri_target=false,
)
_ => ()
}
self.generate_data_uri(target_image, asset_dir_key?)
} else {
self.normalize_web_path(target_image, start?=images_base)
}
}
///|
/// Ruby `media_uri(target, asset_dir_key)`.
pub fn Node::media_uri(
self : Node,
target : String,
asset_dir_key? : String? = Some("imagesdir"),
) -> String {
let base = match asset_dir_key {
Some(k) => self.attr(k, inherited=true)
None => None
}
self.normalize_web_path(target, start?=base)
}
///|
/// Ruby `generate_data_uri(target_image, asset_dir_key)`.
pub fn Node::generate_data_uri(
self : Node,
target_image : String,
asset_dir_key? : String,
) -> String {
let ext = extname(target_image, fallback="")
let mimetype = if ext == "" {
"application/octet-stream"
} else if ext == ".svg" {
"image/svg+xml"
} else {
"image/\{@rb.from(ext, 1)}"
}
let image_path = match asset_dir_key {
Some(k) =>
self.normalize_system_path(
target_image,
start?=self.attr(k, inherited=true),
target_name="image",
)
None => self.normalize_system_path(target_image)
}
match self.document().vfs().read(image_path) {
Some(data) => "data:\{mimetype};base64,\{base64_encode(data)}"
None => {
log_warn("image to embed not found or not readable: \{image_path}")
"data:\{mimetype};base64,"
}
}
}
///|
/// Ruby `normalize_asset_path`. Raises `SecurityError` when the path is
/// outside of the jail and `autocorrect` is false.
pub fn Node::normalize_asset_path(
self : Node,
asset_ref : String,
asset_name? : String = "path",
autocorrect? : Bool = true,
) -> String raise SecurityError {
// Ruby: normalize_system_path asset_ref, @document.base_dir, nil, ...
// (jailed to the base dir unless the safe mode is UNSAFE)
self.normalize_system_path_checked(
asset_ref,
start=self.document().base_dir(),
target_name=asset_name,
recover=autocorrect,
)
}
///|
/// Ruby `normalize_system_path(target, start, jail, opts)` (recovering mode).
pub fn Node::normalize_system_path(
self : Node,
target : String,
start? : String,
jail? : String,
target_name? : String = "path",
) -> String {
self.normalize_system_path_checked(target, start?, jail?, target_name~) catch {
SecurityError(msg) => {
log_error(msg)
target
}
}
}
///|
/// Like `normalize_system_path` but raises on jail violations when `recover` is false.
pub fn Node::normalize_system_path_checked(
self : Node,
target : String,
start? : String,
jail? : String,
target_name? : String = "path",
recover? : Bool = true,
) -> String raise SecurityError {
let doc = self.document()
let pr = doc.path_resolver()
let mut start = start
let mut jail = jail
if doc.safe() < SAFE_SAFE {
start = match start {
Some(s) =>
if pr.is_root(s) {
Some(s)
} else {
Some(file_join(doc.base_dir(), s))
}
None => Some(doc.base_dir())
}
} else {
if start is None {
start = Some(doc.base_dir())
}
if jail is None {
jail = Some(doc.base_dir())
}
}
pr.system_path(Some(target), start?, jail?, target_name~, recover~)
}
///|
/// Ruby `normalize_web_path(target, start, preserve_uri_target)`.
pub fn Node::normalize_web_path(
self : Node,
target : String,
start? : String,
preserve_uri_target? : Bool = true,
) -> String {
if preserve_uri_target && is_uriish(target) {
encode_spaces_in_uri(target)
} else {
self.document().path_resolver().web_path(target, start?)
}
}
///|
/// Reads an asset from the VFS (Ruby `read_asset`).
pub fn Node::read_asset(
self : Node,
path : String,
normalize? : Bool = false,
warn_on_failure? : Bool = false,
label? : String = "file",
) -> String? {
let doc = self.document()
match doc.vfs().read(path) {
Some(data) => {
let text = decode_text(data)
if normalize {
Some(prepare_source_string(text).join("\n"))
} else {
Some(text)
}
}
None => {
if warn_on_failure {
log_warn(
"\{doc.attributes.str("docfile").unwrap_or("")}: \{label} does not exist or cannot be read: \{path}",
)
}
None
}
}
}
///|
/// Reads contents of a local target (URIs are not supported by the pure core).
pub fn Node::read_contents(
self : Node,
target : String,
start? : String,
normalize? : Bool = false,
warn_on_failure? : Bool = true,
warn_if_empty? : Bool = false,
label? : String = "asset",
) -> String? {
let doc = self.document()
let web_start = match start {
Some(s) if is_uriish(s) => true
_ => false
}
if is_uriish(target) || web_start {
let t = if web_start {
doc.path_resolver().web_path(target, start?)
} else {
target
}
if doc.has_attr("allow-uri-read") {
match doc.vfs().read(t) {
Some(data) => {
let text = decode_text(data)
return Some(
if normalize {
prepare_source_string(text).join("\n")
} else {
text
},
)
}
None => {
if warn_on_failure {
log_warn("could not retrieve contents of \{label} at URI: \{t}")
}
return None
}
}
} else {
if warn_on_failure {
log_warn(
"cannot retrieve contents of \{label} at URI: \{t} (allow-uri-read attribute not enabled)",
)
}
return None
}
}
let target = self.normalize_system_path(target, start?, target_name=label)
let contents = self.read_asset(target, normalize~, warn_on_failure~, label~)
match contents {
Some(c) if warn_if_empty && c == "" =>
log_warn("contents of \{label} is empty: \{target}")
_ => ()
}
contents
}