Packages
html_to_markdown
2.28.5
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/lib.rs
#![allow(clippy::let_unit_value, deprecated)]
use std::collections::HashMap;
use html_to_markdown_rs::convert_with_metadata as convert_with_metadata_inner;
use html_to_markdown_rs::convert_with_tables as convert_with_tables_inner;
use html_to_markdown_rs::metadata::{
DocumentMetadata, ExtendedMetadata, HeaderMetadata, ImageMetadata, LinkMetadata, StructuredData,
};
use html_to_markdown_rs::{
ConversionOptions, HtmlExtraction, InlineImage, convert as convert_inner,
convert_with_inline_images as convert_with_inline_images_inner,
};
mod options;
mod profiling;
mod types;
mod visitor;
use options::{
INVALID_OPTION_ERROR, decode_inline_image_config, decode_metadata_config, decode_options_term,
take_invalid_option_message,
};
use types::{
DocumentMetadataTerm, ExtendedMetadataTerm, HeaderMetadataTerm, ImageMetadataTerm, InlineImageTerm,
InlineImageWarningTerm, LinkMetadataTerm, StructuredDataTerm, TableDataTerm, TableExtractionTerm,
};
use rustler::types::binary::{Binary, OwnedBinary};
use rustler::{Encoder, Env, Error, NifResult, ResourceArc, Term};
use std::path::PathBuf;
struct OptionsHandleResource(ConversionOptions);
rustler::init!(
"Elixir.HtmlToMarkdown.Native",
[
convert,
convert_with_options_map,
convert_with_handle,
create_options_handle,
convert_with_inline_images,
convert_with_metadata,
start_profiling,
stop_profiling,
convert_with_visitor,
convert_with_tables
],
load = on_load
);
#[allow(non_local_definitions)]
fn on_load(env: Env, _info: Term) -> bool {
let _ = rustler::resource!(OptionsHandleResource, env);
true
}
mod atoms {
rustler::atoms! {
ok,
error,
invalid_option,
conversion_failed,
atx,
atx_closed,
underlined,
spaces,
tabs,
normalized,
strict,
minimal,
standard,
aggressive,
backslash,
indented,
backticks,
tildes,
img_data_uri,
svg_element,
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert<'a>(env: Env<'a>, html: String) -> NifResult<Term<'a>> {
match profiling::maybe_profile(|| convert_inner(&html, None)) {
Ok(markdown) => Ok((atoms::ok(), markdown).encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_options_map<'a>(env: Env<'a>, html: String, options_term: Term<'a>) -> NifResult<Term<'a>> {
let options = match decode_options_term(options_term) {
Ok(options) => options,
Err(err) => return handle_invalid_option_error(env, err),
};
match profiling::maybe_profile(|| convert_inner(&html, Some(options.clone()))) {
Ok(markdown) => Ok((atoms::ok(), markdown).encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_handle<'a>(
env: Env<'a>,
html: String,
handle: ResourceArc<OptionsHandleResource>,
) -> NifResult<Term<'a>> {
match profiling::maybe_profile(|| convert_inner(&html, Some(handle.0.clone()))) {
Ok(markdown) => Ok((atoms::ok(), markdown).encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif]
fn start_profiling<'a>(env: Env<'a>, output: String, frequency: Option<i32>) -> NifResult<Term<'a>> {
let freq = frequency.unwrap_or(1000);
match profiling::start(PathBuf::from(output), freq) {
Ok(()) => Ok(atoms::ok().encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif]
fn stop_profiling<'a>(env: Env<'a>) -> NifResult<Term<'a>> {
match profiling::stop() {
Ok(()) => Ok(atoms::ok().encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_visitor<'a>(
env: Env<'a>,
html: String,
options_term: Term<'a>,
visitor_pid: Term<'a>,
) -> NifResult<Term<'a>> {
let options = if let Ok(handle) = options_term.decode::<ResourceArc<OptionsHandleResource>>() {
handle.0.clone()
} else {
match decode_options_term(options_term) {
Ok(options) => options,
Err(err) => return handle_invalid_option_error(env, err),
}
};
match visitor::convert_with_visitor(&html, options, env, visitor_pid) {
Ok(markdown) => Ok((atoms::ok(), markdown).encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn create_options_handle<'a>(env: Env<'a>, options_term: Term<'a>) -> NifResult<Term<'a>> {
match decode_options_term(options_term) {
Ok(options) => {
let resource = ResourceArc::new(OptionsHandleResource(options));
Ok((atoms::ok(), resource).encode(env))
}
Err(err) => handle_invalid_option_error(env, err),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_inline_images<'a>(
env: Env<'a>,
html: String,
options_term: Term<'a>,
config_term: Term<'a>,
) -> NifResult<Term<'a>> {
let options = match decode_options_term(options_term) {
Ok(options) => options,
Err(err) => return handle_invalid_option_error(env, err),
};
let config = match decode_inline_image_config(config_term) {
Ok(config) => config,
Err(err) => return handle_invalid_option_error(env, err),
};
let options = options.clone();
let config = config.clone();
match profiling::maybe_profile(|| {
convert_with_inline_images_inner(&html, Some(options.clone()), config.clone(), None)
}) {
Ok(HtmlExtraction {
markdown,
inline_images,
warnings,
}) => {
let images = inline_images
.into_iter()
.map(|image| build_inline_image(env, image))
.collect::<NifResult<Vec<_>>>()?;
let warning_terms: Vec<InlineImageWarningTerm> = warnings
.into_iter()
.map(|warning| InlineImageWarningTerm {
index: warning.index as i64,
message: warning.message,
})
.collect();
Ok((atoms::ok(), (markdown, images, warning_terms)).encode(env))
}
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_metadata<'a>(
env: Env<'a>,
html: String,
options_term: Term<'a>,
config_term: Term<'a>,
) -> NifResult<Term<'a>> {
let options = match decode_options_term(options_term) {
Ok(options) => options,
Err(err) => return handle_invalid_option_error(env, err),
};
let config = match decode_metadata_config(config_term) {
Ok(config) => config,
Err(err) => return handle_invalid_option_error(env, err),
};
let options = options.clone();
let config = config.clone();
match profiling::maybe_profile(|| convert_with_metadata_inner(&html, Some(options.clone()), config.clone(), None)) {
Ok((markdown, metadata)) => Ok((atoms::ok(), (markdown, build_metadata(metadata))).encode(env)),
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
#[rustler::nif(schedule = "DirtyCpu")]
fn convert_with_tables<'a>(
env: Env<'a>,
html: String,
options_term: Term<'a>,
config_term: Term<'a>,
) -> NifResult<Term<'a>> {
let options = match decode_options_term(options_term) {
Ok(options) => options,
Err(err) => return handle_invalid_option_error(env, err),
};
let config = match decode_metadata_config(config_term) {
Ok(config) => config,
Err(err) => return handle_invalid_option_error(env, err),
};
match profiling::maybe_profile(|| convert_with_tables_inner(&html, Some(options.clone()), Some(config.clone()))) {
Ok(result) => {
let tables: Vec<TableDataTerm> = result
.tables
.into_iter()
.map(|t| TableDataTerm {
cells: t.cells,
markdown: t.markdown,
is_header_row: t.is_header_row,
})
.collect();
let metadata = result.metadata.map(build_metadata);
let extraction = TableExtractionTerm {
content: result.content,
metadata,
tables,
};
Ok((atoms::ok(), extraction).encode(env))
}
Err(err) => Ok((atoms::error(), err.to_string()).encode(env)),
}
}
fn build_metadata(metadata: ExtendedMetadata) -> ExtendedMetadataTerm {
ExtendedMetadataTerm {
document: build_document_metadata(metadata.document),
headers: metadata.headers.into_iter().map(build_header_metadata).collect(),
links: metadata.links.into_iter().map(build_link_metadata).collect(),
images: metadata.images.into_iter().map(build_image_metadata).collect(),
structured_data: metadata
.structured_data
.into_iter()
.map(build_structured_data)
.collect(),
}
}
fn build_document_metadata(metadata: DocumentMetadata) -> DocumentMetadataTerm {
DocumentMetadataTerm {
title: metadata.title,
description: metadata.description,
keywords: metadata.keywords,
author: metadata.author,
canonical_url: metadata.canonical_url,
base_href: metadata.base_href,
language: metadata.language,
text_direction: metadata.text_direction.map(|td| td.to_string()),
open_graph: metadata.open_graph.into_iter().collect(),
twitter_card: metadata.twitter_card.into_iter().collect(),
meta_tags: metadata.meta_tags.into_iter().collect(),
}
}
fn build_header_metadata(metadata: HeaderMetadata) -> HeaderMetadataTerm {
HeaderMetadataTerm {
level: metadata.level,
text: metadata.text,
id: metadata.id,
depth: metadata.depth as u64,
html_offset: metadata.html_offset as u64,
}
}
fn build_link_metadata(metadata: LinkMetadata) -> LinkMetadataTerm {
LinkMetadataTerm {
href: metadata.href,
text: metadata.text,
title: metadata.title,
link_type: metadata.link_type.to_string(),
rel: metadata.rel,
attributes: metadata.attributes.into_iter().collect(),
}
}
fn build_image_metadata(metadata: ImageMetadata) -> ImageMetadataTerm {
ImageMetadataTerm {
src: metadata.src,
alt: metadata.alt,
title: metadata.title,
dimensions: metadata.dimensions,
image_type: metadata.image_type.to_string(),
attributes: metadata.attributes.into_iter().collect(),
}
}
fn build_structured_data(metadata: StructuredData) -> StructuredDataTerm {
StructuredDataTerm {
data_type: metadata.data_type.to_string(),
raw_json: metadata.raw_json,
schema_type: metadata.schema_type,
}
}
fn build_inline_image<'a>(env: Env<'a>, image: InlineImage) -> NifResult<InlineImageTerm<'a>> {
let InlineImage {
data,
format,
filename,
description,
dimensions,
source,
attributes,
} = image;
let mut binary = OwnedBinary::new(data.len()).ok_or(Error::BadArg)?;
binary.as_mut_slice().copy_from_slice(&data);
let binary = Binary::from_owned(binary, env);
let mut attr_map = HashMap::with_capacity(attributes.len());
for (key, value) in attributes {
attr_map.insert(key, value);
}
Ok(InlineImageTerm {
data: binary,
format: format.to_string(),
filename,
description,
dimensions,
source: source.to_string(),
attributes: attr_map,
})
}
fn handle_invalid_option_error<'a>(env: Env<'a>, err: Error) -> NifResult<Term<'a>> {
match err {
Error::Atom(atom) if atom == INVALID_OPTION_ERROR => {
let reason = take_invalid_option_message().unwrap_or_else(|| "invalid option".to_string());
Ok((atoms::error(), reason).encode(env))
}
other => Err(other),
}
}