Skip to main content

ftml/render/html/element/
image.rs

1/*
2 * render/html/element/image.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use super::prelude::*;
22use crate::tree::{AttributeMap, FileSource, FloatAlignment, LinkLocation};
23use crate::url::normalize_link;
24
25pub fn render_image(
26    ctx: &mut HtmlContext,
27    source: &FileSource,
28    link: &Option<LinkLocation>,
29    alignment: Option<FloatAlignment>,
30    attributes: &AttributeMap,
31) {
32    debug!(
33        "Rendering image element (source '{}', link {:?}, alignment {}, float {})",
34        source.name(),
35        link,
36        match alignment {
37            Some(image) => image.align.name(),
38            None => "<default>",
39        },
40        match alignment {
41            Some(image) => image.float,
42            None => false,
43        },
44    );
45
46    let source_url = ctx
47        .handle()
48        .get_file_link(source, ctx.info(), ctx.settings());
49
50    match source_url {
51        // Found URL
52        Some(url) => render_image_element(ctx, &url, link, alignment, attributes),
53
54        // Missing or error
55        None => render_image_missing(ctx),
56    }
57}
58
59fn render_image_element(
60    ctx: &mut HtmlContext,
61    image_url: &str,
62    link: &Option<LinkLocation>,
63    alignment: Option<FloatAlignment>,
64    attributes: &AttributeMap,
65) {
66    trace!("Found URL, rendering image (value '{image_url}')");
67
68    match ctx.layout() {
69        Layout::Wikidot => {
70            render_image_element_wikidot(ctx, image_url, link, alignment, attributes);
71        }
72        Layout::Wikijump => {
73            render_image_element_wikijump(ctx, image_url, link, alignment, attributes);
74        }
75    }
76}
77
78/// Render an image block with a Wikidot-compatible DOM.
79///
80/// The structure is thus:
81/// 1. If alignment, wrap in `<div>`. Otherwise nothing.
82/// 2. If link, wrap in `<a>`. Otherwise nothing.
83/// 3. The image itself, `<img>`.
84///
85/// We define the closures in reverse order so
86/// we can properly (conditionally) nest them.
87fn render_image_element_wikidot(
88    ctx: &mut HtmlContext,
89    image_url: &str,
90    link: &Option<LinkLocation>,
91    alignment: Option<FloatAlignment>,
92    attributes: &AttributeMap,
93) {
94    let build_image = |ctx: &mut HtmlContext| {
95        ctx.html().img().attr(attr!(
96            "src" => image_url,
97            "class" => "image",
98            "crossorigin";;
99            attributes,
100        ));
101    };
102
103    let build_link = |ctx: &mut HtmlContext| match link {
104        None => build_image(ctx),
105        Some(link) => {
106            let url = normalize_link(link, ctx.handle());
107            ctx.html()
108                .a()
109                .attr(attr!("href" => &url))
110                .inner(build_image);
111        }
112    };
113
114    match alignment {
115        None => build_link(ctx),
116        Some(align) => {
117            ctx.html()
118                .div()
119                .attr(attr!("class" => "image-container " align.wd_html_class()))
120                .inner(build_link);
121        }
122    }
123}
124
125fn render_image_element_wikijump(
126    ctx: &mut HtmlContext,
127    image_url: &str,
128    link: &Option<LinkLocation>,
129    alignment: Option<FloatAlignment>,
130    attributes: &AttributeMap,
131) {
132    let (space, align_class) = match alignment {
133        Some(align) => (" ", align.wj_html_class()),
134        None => ("", ""),
135    };
136
137    ctx.html()
138        .div()
139        .attr(attr!(
140            "class" => "wj-image-container" space align_class,
141        ))
142        .inner(|ctx| {
143            let build_image = |ctx: &mut HtmlContext| {
144                ctx.html().img().attr(attr!(
145                    "class" => "wj-image",
146                    "src" => image_url,
147                    "crossorigin";;
148                    attributes
149                ));
150            };
151
152            match link {
153                Some(link) => {
154                    let url = normalize_link(link, ctx.handle());
155                    ctx.html()
156                        .a()
157                        .attr(attr!("href" => &url))
158                        .inner(build_image);
159                }
160                None => build_image(ctx),
161            };
162        });
163}
164
165fn render_image_missing(ctx: &mut HtmlContext) {
166    trace!("Image URL unresolved, missing or error");
167
168    let message = ctx
169        .handle()
170        .get_message(ctx.language(), "image-context-bad");
171
172    ctx.html()
173        .div()
174        .attr(attr!("class" => "wj-error-block"))
175        .contents(message);
176}