Laughter.Rewriter (Laughter v0.3.0)
Copy MarkdownBuilds immutable HTML rewrite plans and executes them in one native pass.
alias Laughter.Rewriter
plan =
Rewriter.new()
|> Rewriter.remove("script")
|> Rewriter.set_attribute("a[href]", "rel", "nofollow")
|> Rewriter.remove_attribute("img", "onclick")
{:ok, output} = Rewriter.rewrite(plan, html)Builder functions do not call native code. Plans can be reused and passed between processes. Rules run in registration order for each matching element; the last attribute write wins. Removing an element removes its content too. Selectors match the input HTML, not the output of earlier mutations. Inserted HTML is emitted as-is, not parsed again or matched by later rules.
Content operations
prepend_text/3, append_text/3, before_text/3, after_text/3,
replace_text/3, and set_inner_text/3 escape &, <, and >.
Their _html counterparts insert trusted markup verbatim. Content accepts
UTF-8 iodata, even when the input document uses a different encoding. These
operations are not a sanitizer or a JavaScript/CSS escaping API.
Ordering and conflicts in declarative plans
- Repeated
beforeandappendinsertions preserve registration order; repeatedprependandafterinsertions appear in reverse order. - The last
replaceorremovewins for an element. Attribute changes cannot resurrect it. Surroundingbefore/afterinsertions survive. - Inner operations are ignored on removed, replaced, or void elements.
set_innerreplaces original content and discards earlier inner insertions. Laterprepend/appendoperations are retained.- Replacing or removing an ancestor suppresses output from its original descendants, including their mutations.
Use rewrite/2 for complete documents or stream/3 for lazy chunked input
and output. The parser's memory limit does not bound total input or output
size; streaming has a separate per-write output limit.
The legacy on_element/3 and on_text/3 callback API remains available, but
registrations are process-local and cannot be mixed with declarative rules.
Summary
Functions
Inserts content after matched elements. Inserts HTML verbatim; only pass trusted markup.
Inserts content after matched elements. Escapes &, <, and > before insertion.
Appends content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Appends content inside matched elements. Escapes &, <, and > before insertion.
Inserts content before matched elements. Inserts HTML verbatim; only pass trusted markup.
Inserts content before matched elements. Escapes &, <, and > before insertion.
Cancels a message-driven session and discards pending output.
Grants one output credit to a message-driven session.
Accepts EOF for a message-driven session; requires output credit.
Creates an empty plan without allocating native resources.
Registers a legacy element callback in the calling process.
Registers a legacy text callback in the calling process.
Prepends content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Prepends content inside matched elements. Escapes &, <, and > before insertion.
Removes matched elements, including their content.
Removes an attribute from matched elements. Missing attributes are ignored.
Replaces matched elements and their content. Inserts HTML verbatim; only pass trusted markup.
Replaces matched elements and their content. Escapes &, <, and > before insertion.
Replies to a dynamic element request with a list of mutations.
Rewrites a complete document in one native pass, returning a binary.
Sets an attribute on matched elements. Values are escaped by LOL HTML.
Replaces the content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Replaces the content inside matched elements. Escapes &, <, and > before insertion.
Starts an optional message-driven rewrite session linked to the caller.
Lazily rewrites an enumerable of binary or iodata chunks into binary chunks.
Accepts one bounded input chunk without waiting for handler replies.
Types
@type config() :: t()
@type content_operation() ::
((((((((((:prepend_text | :prepend_html) | :append_text) | :append_html)
| :before_text)
| :before_html)
| :after_text)
| :after_html)
| :replace_text)
| :replace_html)
| :set_inner_text)
| :set_inner_html
@type element_handler() :: (String.t(), [{String.t(), String.t()}] -> [element_mutation()])
@type element_mutation() :: :remove | :noop | {:set_attribute, String.t(), String.t()} | {:remove_attribute, String.t()} | {content_operation(), String.t()}
@type handler_id() :: non_neg_integer()
@type mutation() :: :remove | {:set_attribute, %{name: String.t(), value: String.t()}} | {:remove_attribute, String.t()} | {content_operation(), String.t()}
@type reply_mutation() :: :remove | {:set_attribute, String.t(), String.t()} | {:remove_attribute, String.t()} | {content_operation(), iodata()}
@opaque t()
@type text_handler() :: (String.t(), boolean() -> [text_mutation()])
@type text_mutation() :: :remove | :noop | {:replace_html | :replace_text | :before_html | :before_text | :after_html | :after_text, String.t()}
Functions
Inserts content after matched elements. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Inserts content after matched elements. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
Appends content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Appends content inside matched elements. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
Inserts content before matched elements. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Inserts content before matched elements. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
Cancels a message-driven session and discards pending output.
Grants one output credit to a message-driven session.
Accepts EOF for a message-driven session; requires output credit.
Creates an empty plan without allocating native resources.
Options are :encoding (default "utf-8") and :max_memory (default
1_048_576 bytes, for LOL HTML's internal buffers only). Unknown options and
invalid option types raise ArgumentError; unsupported encodings and invalid
CSS selectors return errors when the plan is executed.
@spec on_element(t(), String.t(), element_handler()) :: handler_id()
Registers a legacy element callback in the calling process.
Use the declarative builders for native execution. Callback registration and rewriting must happen in the same process, as in the original API.
@spec on_text(t(), String.t(), text_handler()) :: handler_id()
Registers a legacy text callback in the calling process.
Prepends content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Prepends content inside matched elements. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
Removes matched elements, including their content.
Removes an attribute from matched elements. Missing attributes are ignored.
Replaces matched elements and their content. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Replaces matched elements and their content. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
@spec reply(pid(), reference(), [reply_mutation()]) :: :ok | {:error, term()}
Replies to a dynamic element request with a list of mutations.
Rewrites a complete document in one native pass, returning a binary.
Declarative plans execute on a dirty CPU scheduler, without worker threads, per-element messages, or Elixir callbacks. Rules are decoded once per call.
Sets an attribute on matched elements. Values are escaped by LOL HTML.
Replaces the content inside matched elements. Inserts HTML verbatim; only pass trusted markup.
Content must be UTF-8 iodata, regardless of the document encoding.
Replaces the content inside matched elements. Escapes &, <, and > before insertion.
Content must be UTF-8 iodata, regardless of the document encoding.
@spec start_link( t(), keyword() ) :: GenServer.on_start()
Starts an optional message-driven rewrite session linked to the caller.
Requires selector: "...". The owner defaults to the caller. See
Laughter.Rewriter.Session for the demand/reply protocol, limits, and supervision.
@spec stream(Enumerable.t(), t(), keyword()) :: Enumerable.t()
Lazily rewrites an enumerable of binary or iodata chunks into binary chunks.
File.stream!("input.html", [], 65_536)
|> Laughter.Rewriter.stream(plan)
|> Stream.into(File.stream!("output.html"))
|> Stream.run()Each enumeration opens a fresh native session and decodes the plan once. Output is emitted as it becomes available; chunk boundaries need not match the input and may split encoded characters. EOF flushes any pending bytes. Early halt or an exception closes the session without flushing. Legacy callbacks are not supported.
Options and bounds
:chunk_size— maximum bytes per native write (default65_536). Larger source chunks are split lazily.:max_output_bytes— maximum buffered output from one native write or EOF flush (default1_048_576). Expansion beyond this limit fails the session instead of accumulating unbounded output.
Limits must be positive integers. Together with the plan's :max_memory,
these bound native parser/output buffering independently of document length.
They do not bound the plan's size, upstream chunk allocations, or output
retained by the consumer. Each iodata source chunk is converted to a binary
before splitting; use bounded upstream chunks for bounded end-to-end memory.
Native errors raise Laughter.Rewriter.Error during enumeration. Previously
emitted output cannot be rolled back. Exceptions from the source or consumer
propagate unchanged, with session cleanup still performed.
Accepts one bounded input chunk without waiting for handler replies.