Packages
html_to_markdown
2.27.1
3.9.0
3.8.3
3.8.2
3.8.1
3.8.0
3.8.0-rc.2
3.8.0-rc.1
3.7.2
3.7.1
3.7.0
3.6.21
3.6.20
3.6.19
3.6.18
3.6.17
3.6.16
3.6.15
3.6.14
3.6.13
3.6.11
3.6.10
3.6.9
3.6.8
3.6.3
3.6.1
3.6.0
3.6.0-rc.25
3.6.0-rc.23
3.6.0-rc.22
3.6.0-rc.21
3.6.0-rc.20
3.6.0-rc.17
3.6.0-rc.16
3.6.0-rc.14
3.6.0-rc.13
3.6.0-rc.12
3.6.0-rc.11
3.6.0-rc.10
3.6.0-rc.9
3.6.0-rc.7
3.6.0-rc.3
3.6.0-rc.2
3.6.0-rc.1
3.5.7
3.5.5
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.4.0
3.4.0-rc.45
3.4.0-rc.44
3.4.0-rc.42
3.4.0-rc.41
3.4.0-rc.40
3.4.0-rc.25
3.4.0-rc.24
3.4.0-rc.23
3.4.0-rc.21
3.4.0-rc.20
3.4.0-rc.18
3.4.0-rc.16
3.4.0-rc.15
3.4.0-rc.14
3.4.0-rc.13
3.4.0-rc.12
3.4.0-rc.11
3.4.0-rc.9
3.4.0-rc.8
3.4.0-rc.2
3.4.0-rc.1
3.3.2
3.2.4
3.2.2
3.2.1
3.2.0
3.1.0
3.0.2
3.0.1
3.0.0
2.30.0
2.29.0
2.28.6
2.28.5
2.28.4
2.28.3
2.28.2
2.28.1
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.3
2.26.2
2.26.1
2.25.1
2.25.0
2.24.6
2.24.5
2.24.4
2.24.3
2.24.1
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.5
2.22.2
2.22.1
2.22.0
2.21.1
2.20.0
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
2.19.2
2.19.1
2.19.0
2.18.0
2.16.1
2.16.0
2.15.0
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.3
2.14.2
2.14.1
2.14.0
2.13.0
2.12.1
2.12.0
2.11.4
2.11.3
2.11.1
2.10.1
2.9.2
2.9.1
2.9.0
2.8.3
High-performance HTML to Markdown converter
Current section
Files
Jump to
Current section
Files
native/html_to_markdown_elixir/src/options.rs
//! Option parsing for Elixir bindings.
use std::cell::RefCell;
use std::collections::HashMap;
use html_to_markdown_rs::metadata::MetadataConfig;
use html_to_markdown_rs::{
CodeBlockStyle, ConversionOptions, ConversionOptionsUpdate, DEFAULT_INLINE_IMAGE_LIMIT, HeadingStyle,
HighlightStyle, InlineImageConfig, InlineImageConfigUpdate, ListIndentType, MetadataConfigUpdate, NewlineStyle,
PreprocessingOptionsUpdate, PreprocessingPreset, WhitespaceMode,
};
use rustler::{Error, NifResult, Term};
pub const INVALID_OPTION_ERROR: &str = "invalid_option";
thread_local! {
static LAST_INVALID_OPTION: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn set_invalid_option_message(message: impl Into<String>) {
LAST_INVALID_OPTION.with(|slot| {
*slot.borrow_mut() = Some(message.into());
});
}
pub fn take_invalid_option_message() -> Option<String> {
LAST_INVALID_OPTION.with(|slot| slot.borrow_mut().take())
}
pub fn bad_option(field: &'static str) -> Error {
bad_option_msg(field, format!("invalid value for {field}"))
}
pub fn bad_option_msg(field: &'static str, message: impl Into<String>) -> Error {
let message = message.into();
set_invalid_option_message(format!("{field}: {message}"));
Error::Atom(INVALID_OPTION_ERROR)
}
pub fn decode_options_term(term: Term) -> NifResult<ConversionOptions> {
if matches!(term.atom_to_string(), Ok(name) if name == "nil") {
return Ok(ConversionOptions::default());
}
let map: HashMap<String, Term> = term
.decode()
.map_err(|_| bad_option_msg("options", "must be provided as a map"))?;
apply_options(map)
}
pub fn decode_metadata_config(term: Term) -> NifResult<MetadataConfig> {
if matches!(term.atom_to_string(), Ok(name) if name == "nil") {
return Ok(MetadataConfig::default());
}
let map: HashMap<String, Term> = term
.decode()
.map_err(|_| bad_option_msg("metadata_config", "must be provided as a map"))?;
let mut update = MetadataConfigUpdate::default();
for (key, value) in map.into_iter() {
match key.as_str() {
"extract_document" => update.extract_document = Some(decode_bool(value, "extract_document")?),
"extract_headers" => update.extract_headers = Some(decode_bool(value, "extract_headers")?),
"extract_links" => update.extract_links = Some(decode_bool(value, "extract_links")?),
"extract_images" => update.extract_images = Some(decode_bool(value, "extract_images")?),
"extract_structured_data" => {
update.extract_structured_data = Some(decode_bool(value, "extract_structured_data")?)
}
"max_structured_data_size" => {
update.max_structured_data_size = Some(decode_positive_integer(value, "max_structured_data_size")?)
}
_ => {}
}
}
Ok(MetadataConfig::from(update))
}
fn apply_options(map: HashMap<String, Term>) -> NifResult<ConversionOptions> {
let mut update = ConversionOptionsUpdate::default();
for (key, value) in map.into_iter() {
match key.as_str() {
"heading_style" => update.heading_style = Some(parse_heading_style(value)?),
"list_indent_type" => update.list_indent_type = Some(parse_list_indent_type(value)?),
"list_indent_width" => {
update.list_indent_width = Some(decode_positive_integer(value, "list_indent_width")?)
}
"bullets" => update.bullets = Some(decode_string(value, "bullets")?),
"strong_em_symbol" => {
let symbol = decode_string(value, "strong_em_symbol")?;
let mut chars = symbol.chars();
let ch = chars.next().ok_or_else(|| bad_option("strong_em_symbol"))?;
update.strong_em_symbol = Some(ch);
}
"escape_asterisks" => update.escape_asterisks = Some(decode_bool(value, "escape_asterisks")?),
"escape_underscores" => update.escape_underscores = Some(decode_bool(value, "escape_underscores")?),
"escape_misc" => update.escape_misc = Some(decode_bool(value, "escape_misc")?),
"escape_ascii" => update.escape_ascii = Some(decode_bool(value, "escape_ascii")?),
"code_language" => update.code_language = Some(decode_string(value, "code_language")?),
"encoding" => update.encoding = Some(decode_string(value, "encoding")?),
"autolinks" => update.autolinks = Some(decode_bool(value, "autolinks")?),
"default_title" => update.default_title = Some(decode_bool(value, "default_title")?),
"keep_inline_images_in" => {
update.keep_inline_images_in = Some(decode_string_list(value, "keep_inline_images_in")?)
}
"br_in_tables" => update.br_in_tables = Some(decode_bool(value, "br_in_tables")?),
"hocr_spatial_tables" => update.hocr_spatial_tables = Some(decode_bool(value, "hocr_spatial_tables")?),
"highlight_style" => update.highlight_style = Some(parse_highlight_style(value)?),
"extract_metadata" => update.extract_metadata = Some(decode_bool(value, "extract_metadata")?),
"whitespace_mode" => update.whitespace_mode = Some(parse_whitespace_mode(value)?),
"strip_newlines" => update.strip_newlines = Some(decode_bool(value, "strip_newlines")?),
"wrap" => update.wrap = Some(decode_bool(value, "wrap")?),
"wrap_width" => update.wrap_width = Some(decode_positive_integer(value, "wrap_width")?),
"strip_tags" => update.strip_tags = Some(decode_string_list(value, "strip_tags")?),
"preserve_tags" => update.preserve_tags = Some(decode_string_list(value, "preserve_tags")?),
"convert_as_inline" => update.convert_as_inline = Some(decode_bool(value, "convert_as_inline")?),
"sub_symbol" => update.sub_symbol = Some(decode_string(value, "sub_symbol")?),
"sup_symbol" => update.sup_symbol = Some(decode_string(value, "sup_symbol")?),
"newline_style" => update.newline_style = Some(parse_newline_style(value)?),
"code_block_style" => update.code_block_style = Some(parse_code_block_style(value)?),
"preprocessing" => update.preprocessing = Some(decode_preprocessing(value)?),
"debug" => update.debug = Some(decode_bool(value, "debug")?),
_ => {}
}
}
Ok(ConversionOptions::from(update))
}
fn decode_preprocessing(term: Term) -> NifResult<PreprocessingOptionsUpdate> {
let map: HashMap<String, Term> = term
.decode()
.map_err(|_| bad_option_msg("preprocessing", "must be provided as a map"))?;
let mut update = PreprocessingOptionsUpdate::default();
for (key, value) in map.into_iter() {
match key.as_str() {
"enabled" => update.enabled = Some(decode_bool(value, "preprocessing.enabled")?),
"preset" => update.preset = Some(parse_preset(value)?),
"remove_navigation" => {
update.remove_navigation = Some(decode_bool(value, "preprocessing.remove_navigation")?)
}
"remove_forms" => update.remove_forms = Some(decode_bool(value, "preprocessing.remove_forms")?),
_ => {}
}
}
Ok(update)
}
pub fn decode_inline_image_config(term: Term) -> NifResult<InlineImageConfig> {
if matches!(term.atom_to_string(), Ok(name) if name == "nil") {
return Ok(InlineImageConfig::new(DEFAULT_INLINE_IMAGE_LIMIT));
}
let map: HashMap<String, Term> = term
.decode()
.map_err(|_| bad_option_msg("inline_image_config", "must be provided as a map"))?;
let max_bytes = match map.get("max_decoded_size_bytes") {
Some(value) => match value.decode::<i64>().map_err(|_| {
bad_option_msg(
"inline_image_config.max_decoded_size_bytes",
"must be a positive integer",
)
})? {
v if v > 0 => v as u64,
_ => {
return Err(bad_option_msg(
"max_decoded_size_bytes",
"max_decoded_size_bytes must be greater than zero",
));
}
},
None => DEFAULT_INLINE_IMAGE_LIMIT,
};
let mut update = InlineImageConfigUpdate::default();
update.max_decoded_size_bytes = Some(max_bytes);
if let Some(value) = map.get("filename_prefix") {
let prefix = value
.decode::<String>()
.map_err(|_| bad_option_msg("inline_image_config.filename_prefix", "must be a string"))?;
if !prefix.trim().is_empty() {
update.filename_prefix = Some(prefix);
}
}
if let Some(value) = map.get("capture_svg") {
update.capture_svg = Some(
value
.decode::<bool>()
.map_err(|_| bad_option_msg("inline_image_config.capture_svg", "must be a boolean"))?,
);
}
if let Some(value) = map.get("infer_dimensions") {
update.infer_dimensions = Some(
value
.decode::<bool>()
.map_err(|_| bad_option_msg("inline_image_config.infer_dimensions", "must be a boolean"))?,
);
}
Ok(InlineImageConfig::from_update(update))
}
fn decode_string_list(term: Term, field: &'static str) -> NifResult<Vec<String>> {
let list: Vec<Term> = term
.decode()
.map_err(|_| bad_option_msg(field, "must be provided as a list of strings"))?;
let mut values = Vec::with_capacity(list.len());
for entry in list {
let value = entry
.decode::<String>()
.map_err(|_| bad_option_msg(field, "must contain only strings"))?;
values.push(value);
}
Ok(values)
}
fn decode_positive_integer(term: Term, field: &'static str) -> NifResult<usize> {
let value = term
.decode::<i64>()
.map_err(|_| bad_option_msg(field, format!("{field} must be a positive integer")))?;
if value <= 0 {
Err(bad_option_msg(field, format!("{field} must be greater than zero")))
} else {
Ok(value as usize)
}
}
fn parse_heading_style(term: Term) -> NifResult<HeadingStyle> {
let value = decode_atom_or_string(term)?;
match value.as_str() {
"atx" => Ok(HeadingStyle::Atx),
"atx_closed" => Ok(HeadingStyle::AtxClosed),
"underlined" => Ok(HeadingStyle::Underlined),
_ => Err(bad_option_msg("heading_style", format!("invalid value: {value}"))),
}
}
fn parse_list_indent_type(term: Term) -> NifResult<ListIndentType> {
let value = decode_atom_or_string(term)?;
match value.as_str() {
"spaces" => Ok(ListIndentType::Spaces),
"tabs" => Ok(ListIndentType::Tabs),
_ => Err(bad_option_msg("list_indent_type", format!("invalid value: {value}"))),
}
}
fn parse_highlight_style(term: Term) -> NifResult<HighlightStyle> {
let value = decode_atom_or_string(term)?.replace('-', "_");
match value.as_str() {
"double_equal" => Ok(HighlightStyle::DoubleEqual),
"html" => Ok(HighlightStyle::Html),
"bold" => Ok(HighlightStyle::Bold),
"none" => Ok(HighlightStyle::None),
_ => Err(bad_option_msg("highlight_style", format!("invalid value: {value}"))),
}
}
fn parse_whitespace_mode(term: Term) -> NifResult<WhitespaceMode> {
let value = decode_atom_or_string(term)?;
match value.as_str() {
"normalized" => Ok(WhitespaceMode::Normalized),
"strict" => Ok(WhitespaceMode::Strict),
_ => Err(bad_option_msg("whitespace_mode", format!("invalid value: {value}"))),
}
}
fn parse_newline_style(term: Term) -> NifResult<NewlineStyle> {
let value = decode_atom_or_string(term)?;
match value.as_str() {
"spaces" => Ok(NewlineStyle::Spaces),
"backslash" => Ok(NewlineStyle::Backslash),
_ => Err(bad_option_msg("newline_style", format!("invalid value: {value}"))),
}
}
fn parse_code_block_style(term: Term) -> NifResult<CodeBlockStyle> {
let value = decode_atom_or_string(term)?;
match value.as_str() {
"indented" => Ok(CodeBlockStyle::Indented),
"backticks" => Ok(CodeBlockStyle::Backticks),
"tildes" => Ok(CodeBlockStyle::Tildes),
_ => Err(bad_option_msg("code_block_style", format!("invalid value: {value}"))),
}
}
fn parse_preset(term: Term) -> NifResult<PreprocessingPreset> {
let value = decode_atom_or_string(term)?.replace('-', "_");
match value.as_str() {
"minimal" => Ok(PreprocessingPreset::Minimal),
"aggressive" => Ok(PreprocessingPreset::Aggressive),
"standard" => Ok(PreprocessingPreset::Standard),
_ => Err(bad_option_msg(
"preprocessing.preset",
format!("invalid value: {value}"),
)),
}
}
fn decode_bool(term: Term, field: &'static str) -> NifResult<bool> {
term.decode::<bool>()
.map_err(|_| bad_option_msg(field, "must be a boolean"))
}
fn decode_string(term: Term, field: &'static str) -> NifResult<String> {
term.decode::<String>()
.map_err(|_| bad_option_msg(field, "must be a string"))
}
fn decode_atom_or_string(term: Term) -> NifResult<String> {
if let Ok(atom) = term.atom_to_string() {
Ok(atom)
} else {
term.decode::<String>()
}
}