use core::fmt;
use crate::pipeline::lex;
use crate::syntax::DirectiveKind;
use crate::syntax::ast::{LexOutput, Node, NodeRef, NodeStore};
use crate::render::render_node::render;
use crate::render::serialize::{DirectiveNormalization, SerializeOptions, serialize_with};
use crate::render::spelling::html::{RenderState, escape_text_chunk};
use crate::render::walk::{NewlineSink, SentinelKind, WalkSink, walk_with_newlines};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderOptions {
pub(crate) directives: DirectiveNormalization,
}
impl RenderOptions {
#[must_use]
pub fn directives(mut self, level: DirectiveNormalization) -> Self {
self.directives = level;
self
}
}
#[must_use]
pub(crate) fn render_html_normalized(out: &LexOutput, level: DirectiveNormalization) -> String {
let normalized = serialize_with(out, SerializeOptions { directives: level });
render_html(&lex(&normalized))
}
#[must_use]
pub(crate) fn render_html(out: &LexOutput) -> String {
let mut s = String::with_capacity(out.normalized.len().saturating_mul(2));
render_html_into(out, &mut s).expect("writing to String never fails");
s
}
pub(crate) fn render_html_into<W: fmt::Write>(out: &LexOutput, writer: &mut W) -> fmt::Result {
let mut sink = HtmlSink::<_, false> {
store: &out.store,
out: writer,
state: RenderState::default(),
};
walk_with_newlines(out, &mut sink)?;
sink.finish()
}
pub(super) fn render_inline_source<W: fmt::Write>(source: &str, writer: &mut W) -> fmt::Result {
let output = lex(source);
let mut sink = HtmlSink::<_, true> {
store: &output.store,
out: writer,
state: RenderState::default(),
};
walk_with_newlines(&output, &mut sink)
}
struct HtmlSink<'a, W: fmt::Write, const INLINE: bool> {
store: &'a NodeStore,
out: &'a mut W,
state: RenderState,
}
impl<W: fmt::Write, const INLINE: bool> HtmlSink<'_, W, INLINE> {
fn finish(&mut self) -> fmt::Result {
if INLINE {
Ok(())
} else {
self.state.finish(self.out)
}
}
}
impl<W: fmt::Write, const INLINE: bool> WalkSink for HtmlSink<'_, W, INLINE> {
fn on_text(&mut self, text: &str) -> fmt::Result {
if INLINE {
return escape_text_chunk(text, self.out);
}
self.state.ensure_in_paragraph(self.out)?;
escape_text_chunk(text, self.out)
}
fn on_node(&mut self, kind: SentinelKind, node: NodeRef) -> fmt::Result {
if INLINE {
return match (kind, node) {
(SentinelKind::Inline, NodeRef::Inline(n))
| (SentinelKind::BlockLeaf, NodeRef::BlockLeaf(n)) => {
render(n, self.store, self.out)
}
_ => Ok(()),
};
}
match (kind, node) {
(SentinelKind::Inline, NodeRef::Inline(n)) => {
self.state.ensure_in_paragraph(self.out)?;
match &n {
Node::Directive(a) if a.kind == DirectiveKind::WarichuOpen => {
self.state.open_warichu(self.out)
}
Node::Directive(a) if a.kind == DirectiveKind::WarichuClose => {
self.state.close_warichu(self.out)
}
_ => render(n, self.store, self.out),
}
}
(SentinelKind::BlockLeaf, NodeRef::BlockLeaf(n)) => {
self.state.before_block_emit(self.out)?;
render(n, self.store, self.out)?;
self.state.after_block_emit();
Ok(())
}
(SentinelKind::BlockOpen, NodeRef::BlockOpen(open)) => {
self.state.open_container(open, self.out)
}
(SentinelKind::BlockClose, NodeRef::BlockClose(close)) => {
self.state.close_container(close.is_inline(), self.out)
}
_ => Ok(()),
}
}
}
impl<W: fmt::Write, const INLINE: bool> NewlineSink for HtmlSink<'_, W, INLINE> {
fn on_newline(&mut self, next: Option<u8>) -> fmt::Result {
if INLINE {
return next.map_or(Ok(()), |_| self.out.write_str("<br />\n"));
}
match next {
Some(b'\n') => self.state.close_paragraph(self.out),
Some(_) if self.state.in_paragraph => self.out.write_str("<br />\n"),
Some(_) | None => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalized_render_emits_actual_paragraph_html() {
let out = lex("Hello.");
assert_eq!(
render_html_normalized(&out, DirectiveNormalization::Canonical),
"<p>Hello.</p>\n",
);
}
#[test]
fn render_options_directives_changes_near_miss_html() {
let document = crate::parse("本文[#ゴチック]続き").expect("source parses");
let snapshot = document.snapshot();
let default = snapshot.to_html_with(RenderOptions::default());
let canonical = snapshot
.to_html_with(RenderOptions::default().directives(DirectiveNormalization::Canonical));
assert_ne!(canonical, default);
assert!(default.contains("aozora-directive"));
assert!(!canonical.contains("aozora-directive"));
}
#[test]
fn non_warichu_inline_directive_renders_via_emitter_not_close_warichu() {
let out = lex("本文[#入力者注(5)]続き");
assert_eq!(
render_html(&out),
"<p>本文<sup class=\"aozora-editor-note\">注5</sup>続き</p>\n",
);
}
#[test]
fn forward_format_preserves_nested_ruby() {
let out = lex("二年程|經《た》つうちに[#「二年程」~「つうちに」に傍点]");
let html = render_html(&out);
assert!(html.contains("<em class=\"aozora-bouten"));
assert!(html.contains("<ruby>經<rp>(</rp><rt>た</rt>"));
assert!(!html.contains("|經《た》"), "html: {html}");
}
}