///|
/// Configuration shared by block, block macro and inline macro processors
/// (Ruby extension `config` Hash).
pub(all) struct ExtensionConfig {
mut content_model : ContentModel?
mut positional_attrs : Array[String]
mut default_attrs : Array[(String, String)]
mut contexts : Array[Context]
mut format : String? // inline macro: "short" or "long"
mut regexp : @regex.Regex?
}
///|
pub fn ExtensionConfig::new(
content_model? : ContentModel,
positional_attrs? : Array[String] = [],
default_attrs? : Array[(String, String)] = [],
contexts? : Array[Context] = [],
format? : String,
regexp? : @regex.Regex,
) -> ExtensionConfig {
{ content_model, positional_attrs, default_attrs, contexts, format, regexp, }
}
///|
pub(all) struct Preprocessor {
process : (Node, Reader) -> Reader? raise
}
///|
pub(all) struct TreeProcessor {
process : (Node) -> Node? raise
}
///|
pub(all) struct Postprocessor {
process : (Node, String) -> String raise
}
///|
pub(all) struct DocinfoProcessor {
location : String
process : (Node) -> String? raise
}
///|
pub(all) struct IncludeProcessor {
handles : (Node, String) -> Bool
process : (Node, Reader, String, Attributes) -> Unit raise
}
///|
pub(all) struct BlockProcessor {
name : String
config : ExtensionConfig
process : (Node, Reader, Attributes) -> Node? raise
}
///|
pub(all) struct BlockMacroProcessor {
name : String
config : ExtensionConfig
process : (Node, String, Attributes) -> Node? raise
}
///|
/// Result of an inline macro: a node to convert, or literal text.
pub(all) enum InlineResult {
InlineNode(Node)
InlineText(String)
}
///|
pub(all) struct InlineMacroProcessor {
name : String
config : ExtensionConfig
process : (Node, String, Attributes) -> InlineResult? raise
}
///|
/// An extension registry (Ruby `Extensions::Registry`). Register processors
/// directly or through groups (run on activation); pass the registry with
/// `Options::new(extensions=...)` or register groups globally with
/// `register_global_extensions`.
pub struct Extensions {
priv preprocessor_list : Array[Preprocessor]
priv tree_processor_list : Array[TreeProcessor]
priv postprocessor_list : Array[Postprocessor]
priv docinfo_processor_list : Array[DocinfoProcessor]
priv include_processor_list : Array[IncludeProcessor]
priv block_map : Map[String, BlockProcessor]
priv block_macro_map : Map[String, BlockMacroProcessor]
priv inline_macro_map : Map[String, InlineMacroProcessor]
priv groups : Array[(Extensions) -> Unit]
mut document : Node?
}
///|
/// Creates an empty registry, optionally with a group to run on activation.
pub fn Extensions::new(group? : (Extensions) -> Unit) -> Extensions {
{
preprocessor_list: [],
tree_processor_list: [],
postprocessor_list: [],
docinfo_processor_list: [],
include_processor_list: [],
block_map: {},
block_macro_map: {},
inline_macro_map: {},
groups: match group {
Some(g) => [g]
None => []
},
document: None,
}
}
///|
/// Adds a group (a function that registers processors on activation).
pub fn Extensions::add_group(
self : Extensions,
group : (Extensions) -> Unit,
) -> Unit {
self.groups.push(group)
}
///|
/// Activates the registry for a document: returns a fresh registry containing
/// the direct registrations plus those made by the global and own groups.
pub fn Extensions::activate(self : Extensions, doc : Node) -> Extensions {
let e : Extensions = {
preprocessor_list: self.preprocessor_list.copy(),
tree_processor_list: self.tree_processor_list.copy(),
postprocessor_list: self.postprocessor_list.copy(),
docinfo_processor_list: self.docinfo_processor_list.copy(),
include_processor_list: self.include_processor_list.copy(),
block_map: self.block_map.copy(),
block_macro_map: self.block_macro_map.copy(),
inline_macro_map: self.inline_macro_map.copy(),
groups: [],
document: Some(doc),
}
match global_extensions_ref.val {
Some(g) if !physical_equal(g, self) =>
for grp in g.groups {
grp(e)
}
_ => ()
}
for grp in self.groups {
grp(e)
}
e
}
///|
/// Registers a preprocessor. With `prefer`, it runs before the others.
pub fn Extensions::preprocessor(
self : Extensions,
process : (Node, Reader) -> Reader? raise,
prefer? : Bool = false,
) -> Unit {
let p : Preprocessor = { process, }
if prefer {
self.preprocessor_list.insert(0, p)
} else {
self.preprocessor_list.push(p)
}
}
///|
/// Registers a tree processor; returning a document replaces the document.
pub fn Extensions::tree_processor(
self : Extensions,
process : (Node) -> Node? raise,
prefer? : Bool = false,
) -> Unit {
let p : TreeProcessor = { process, }
if prefer {
self.tree_processor_list.insert(0, p)
} else {
self.tree_processor_list.push(p)
}
}
///|
/// Registers a postprocessor (transforms the converted output).
pub fn Extensions::postprocessor(
self : Extensions,
process : (Node, String) -> String raise,
prefer? : Bool = false,
) -> Unit {
let p : Postprocessor = { process, }
if prefer {
self.postprocessor_list.insert(0, p)
} else {
self.postprocessor_list.push(p)
}
}
///|
/// Registers an include processor.
pub fn Extensions::include_processor(
self : Extensions,
process : (Node, Reader, String, Attributes) -> Unit raise,
handles? : (Node, String) -> Bool = (_, _) => true,
prefer? : Bool = false,
) -> Unit {
let p = { handles, process, }
if prefer {
self.include_processor_list.insert(0, p)
} else {
self.include_processor_list.push(p)
}
}
///|
/// Registers a docinfo processor for `location` (`head` or `footer`).
pub fn Extensions::docinfo_processor(
self : Extensions,
process : (Node) -> String? raise,
location? : String = "head",
prefer? : Bool = false,
) -> Unit {
let p = { location, process, }
if prefer {
self.docinfo_processor_list.insert(0, p)
} else {
self.docinfo_processor_list.push(p)
}
}
///|
/// Registers a block processor for a style `name` on `contexts`
/// (default: open and paragraph).
pub fn Extensions::block(
self : Extensions,
name : String,
process : (Node, Reader, Attributes) -> Node? raise,
contexts? : Array[Context] = [Open, Paragraph],
content_model? : ContentModel = Compound,
positional_attrs? : Array[String] = [],
default_attrs? : Array[(String, String)] = [],
) -> Unit {
self.block_map[name] = {
name,
config: ExtensionConfig::new(
content_model~,
positional_attrs~,
default_attrs~,
contexts~,
),
process,
}
}
///|
/// Registers a block macro processor (`name::target[attrs]`).
pub fn Extensions::block_macro(
self : Extensions,
name : String,
process : (Node, String, Attributes) -> Node? raise,
content_model? : ContentModel = Attributes,
positional_attrs? : Array[String] = [],
default_attrs? : Array[(String, String)] = [],
) -> Unit raise ArgumentError {
if !macro_name_rx.matches(name) {
raise ArgumentError("invalid name for block macro: \{name}")
}
self.block_macro_map[name] = {
name,
config: ExtensionConfig::new(
content_model~,
positional_attrs~,
default_attrs~,
),
process,
}
}
///|
/// Registers an inline macro processor (`name:target[attrs]`, or `name:[attrs]`
/// with `format="short"`, or a custom `regexp`).
pub fn Extensions::inline_macro(
self : Extensions,
name : String,
process : (Node, String, Attributes) -> InlineResult? raise,
format? : String,
regexp? : @regex.Regex,
content_model? : ContentModel = Attributes,
positional_attrs? : Array[String] = [],
default_attrs? : Array[(String, String)] = [],
) -> Unit raise ArgumentError {
if regexp is None && !macro_name_rx.matches(name) {
raise ArgumentError("invalid name for inline macro: \{name}")
}
self.inline_macro_map[name] = {
name,
config: ExtensionConfig::new(
content_model~,
positional_attrs~,
default_attrs~,
format?,
regexp?,
),
process,
}
}
///|
fn Extensions::preprocessors(self : Extensions) -> Array[Preprocessor] {
self.preprocessor_list
}
///|
fn Extensions::tree_processors(self : Extensions) -> Array[TreeProcessor] {
self.tree_processor_list
}
///|
fn Extensions::postprocessors(self : Extensions) -> Array[Postprocessor] {
self.postprocessor_list
}
///|
fn Extensions::include_processors(self : Extensions) -> Array[IncludeProcessor] {
self.include_processor_list
}
///|
fn Extensions::has_blocks(self : Extensions) -> Bool {
!self.block_map.is_empty()
}
///|
fn Extensions::has_block_macros(self : Extensions) -> Bool {
!self.block_macro_map.is_empty()
}
///|
fn Extensions::inline_macros(self : Extensions) -> Array[InlineMacroProcessor] {
self.inline_macro_map.values().collect()
}
///|
fn Extensions::docinfo_processors_for(
self : Extensions,
location : String,
) -> Array[DocinfoProcessor] {
self.docinfo_processor_list.filter(p => p.location == location)
}
///|
fn Extensions::registered_for_block(
self : Extensions,
name : String,
context : Context,
) -> BlockProcessor? {
match self.block_map.get(name) {
Some(p) if p.config.contexts.contains(context) => Some(p)
_ => None
}
}
///|
fn Extensions::registered_for_block_macro(
self : Extensions,
name : String,
) -> BlockMacroProcessor? {
self.block_macro_map.get(name)
}
///|
let global_extensions_ref : Ref[Extensions?] = { val: None, }
///|
/// Registers a global extension group (Ruby `Asciidoctor::Extensions.register`).
pub fn register_global_extensions(group : (Extensions) -> Unit) -> Unit {
let e = match global_extensions_ref.val {
Some(e) => e
None => {
let e = Extensions::new()
global_extensions_ref.val = Some(e)
e
}
}
e.groups.push(group)
}
///|
/// Removes all global extension groups.
pub fn unregister_all_global_extensions() -> Unit {
global_extensions_ref.val = None
}
///|
fn global_extensions() -> Extensions? {
global_extensions_ref.val
}
// ---------------------------------------------------------------------------
// Processor helpers (Ruby `Extensions::Processor#create_*`, `parse_content`)
///|
/// Creates a block (Ruby `create_block parent, context, source, attrs, opts`).
pub fn create_block(
parent : Node,
context : Context,
source : Array[String]?,
attrs : Attributes,
content_model? : ContentModel,
subs? : BlockSubs,
) -> Node {
Node::new_block(
parent,
context,
content_model?,
subs?,
source?,
attributes=attrs,
)
}
///|
/// Creates a paragraph block from lines.
pub fn create_paragraph(
parent : Node,
source : Array[String],
attrs : Attributes,
) -> Node {
create_block(parent, Paragraph, Some(source), attrs)
}
///|
/// Creates an open block (children parsed with `parse_content`).
pub fn create_open_block(
parent : Node,
source : Array[String]?,
attrs : Attributes,
) -> Node {
create_block(parent, Open, source, attrs)
}
///|
pub fn create_example_block(
parent : Node,
source : Array[String]?,
attrs : Attributes,
) -> Node {
create_block(parent, Example, source, attrs)
}
///|
pub fn create_pass_block(
parent : Node,
source : Array[String],
attrs : Attributes,
) -> Node {
create_block(parent, Pass, Some(source), attrs)
}
///|
pub fn create_listing_block(
parent : Node,
source : Array[String],
attrs : Attributes,
) -> Node {
create_block(parent, Listing, Some(source), attrs)
}
///|
pub fn create_literal_block(
parent : Node,
source : Array[String],
attrs : Attributes,
) -> Node {
create_block(parent, Literal, Some(source), attrs)
}
///|
/// Creates a list (ulist, olist, dlist or colist).
pub fn create_list(
parent : Node,
context : Context,
attrs? : Attributes,
) -> Node {
let list = Node::new_list(parent, context)
match attrs {
Some(a) => list.update_attributes(a)
None => ()
}
list
}
///|
pub fn create_list_item(parent : Node, text? : String) -> Node {
Node::new_list_item(parent, text?)
}
///|
/// Creates an image block; `attrs` must contain `target`.
pub fn create_image_block(
parent : Node,
attrs : Attributes,
) -> Node raise ArgumentError {
guard attrs.str("target") is Some(target) else {
raise ArgumentError(
"Unable to create an image block, target attribute is required",
)
}
if !attrs.truthy("alt") {
let default_alt = basename(target, drop_ext=true)
.replace_all(old="_", new=" ")
.replace_all(old="-", new=" ")
attrs.set_str("default-alt", default_alt)
attrs.set_str("alt", default_alt)
}
let title = if attrs.contains("title") {
attrs.remove_str("title")
} else {
None
}
let block = create_block(parent, Image, None, attrs)
match title {
Some(t) => {
block.set_title(Some(t))
block.assign_caption(
attrs.remove_str("caption"),
caption_context="figure",
)
}
None => ()
}
block
}
///|
/// Creates an inline node (quoted nodes default to type `unquoted`).
pub fn create_inline(
parent : Node,
context : Context,
text : String?,
type_? : String,
target? : String,
id? : String,
attributes? : Attributes,
) -> Node {
let type_ = match type_ {
Some(t) => Some(t)
None => if context == Quoted { Some("unquoted") } else { None }
}
Node::new_inline(parent, context, text?, type_?, target?, id?, attributes?)
}
///|
/// Creates an anchor (link or xref) inline node.
pub fn create_anchor(
parent : Node,
text : String?,
type_ : String,
target? : String,
id? : String,
attributes? : Attributes,
) -> Node {
create_inline(parent, Anchor, text, type_~, target?, id?, attributes?)
}
///|
/// Creates a section (Ruby `create_section parent, title, attrs, opts`).
pub fn create_section(
parent : Node,
title : String,
attrs : Attributes,
level? : Int,
numbered? : Bool,
) -> Node {
let doc = parent.document()
let doctype = doc.doctype()
let book = doctype == "book"
let mut level = level.unwrap_or(parent.level + 1)
let mut sectname = "section"
let mut special = false
let style = attrs.remove_str("style")
match style {
Some(s) =>
if book && s == "abstract" {
sectname = "chapter"
level = 1
} else {
sectname = s
special = true
if level == 0 {
level = 1
}
}
None =>
if book {
sectname = if level == 0 {
"part"
} else if level > 1 {
"section"
} else {
"chapter"
}
} else if doctype == "manpage" && @rb.downcase_ascii(title) == "synopsis" {
sectname = "synopsis"
special = true
}
}
let sect = Node::new_section(Some(parent), level~)
sect.set_title(Some(title))
sect.sectname = Some(sectname)
if special {
sect.special = true
if numbered.unwrap_or(style == Some("appendix")) {
sect.numbered = Numbered
} else if numbered is None && doc.has_attr("sectnums", expected="all") {
sect.numbered = if book && level == 1 {
NumberedChapter
} else {
Numbered
}
}
} else if level > 0 {
if numbered.unwrap_or(doc.has_attr("sectnums")) {
sect.numbered = if sect.special {
if parent.numbered != NotNumbered {
Numbered
} else {
NotNumbered
}
} else {
Numbered
}
}
} else if numbered.unwrap_or(book && doc.has_attr("partnums")) {
sect.numbered = Numbered
}
match attrs.get("id") {
Some(Bool(false)) => attrs.remove("id") |> ignore
v => {
let id = match v {
Some(Str(i)) => Some(i)
_ =>
if doc.has_attr("sectids") {
Some(generate_section_id(sect.title().unwrap_or(""), doc))
} else {
None
}
}
sect.id = id
match id {
Some(i) => attrs.set_str("id", i)
None => attrs.set("id", Nil)
}
}
}
sect.update_attributes(attrs)
sect
}
///|
/// Parses AsciiDoc lines as child blocks of `parent` (Ruby `parse_content`).
pub fn parse_content(
parent : Node,
content : Array[String],
attributes? : Attributes,
) -> Node {
parse_blocks(Reader::new(content), parent, attributes)
parent
}
///|
/// Parses the remaining lines of `reader` as child blocks of `parent`.
pub fn parse_content_from_reader(
parent : Node,
reader : Reader,
attributes? : Attributes,
) -> Node {
parse_blocks(reader, parent, attributes)
parent
}
///|
/// Parses an attribute list for an extension (Ruby `Processor#parse_attributes`).
pub fn parse_extension_attributes(
block : Node,
attrlist : String,
positional_attributes? : Array[String] = [],
sub_attributes? : Bool = false,
) -> Attributes {
if attrlist == "" {
return Attributes::new()
}
let attrlist = if sub_attributes && attrlist.contains("{") {
block.sub_attributes(attrlist)
} else {
attrlist
}
parse_attribute_list(
attrlist,
positional_attrs=positional_attributes.map(x => Some(x)),
)
}