root/src/wiki_page.rs

// wiki_page.rs
//
// Copyright 2025 nee <nee-git@hidamari.blue>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::booru::WikiEntry;
use crate::data::*;
use crate::send;
use crate::sidebar_booru_select::make_booru_selection_list;

use adw::glib::property::PropertySet;
use adw::prelude::*;
use gtk::glib;
use gtk::glib::Properties;
use gtk::glib::clone;
use gtk::subclass::prelude::*;
use std::cell::{Cell, RefCell};

mod imp {
    use super::*;

    #[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::WikiPage)]
    #[template(resource = "/blue/hidamari/boorus/ui/wiki_page.ui")]
    pub struct WikiPage {
        #[template_child]
        pub split: TemplateChild<adw::OverlaySplitView>,
        #[template_child]
        pub window: TemplateChild<gtk::ScrolledWindow>,

        #[template_child]
        pub search: TemplateChild<gtk::SearchEntry>,
        #[template_child]
        pub submit_search: TemplateChild<gtk::Button>,

        #[template_child]
        pub booru_list: TemplateChild<adw::Bin>,
        #[template_child]
        pub wiki_history: TemplateChild<gtk::ListBox>,

        #[template_child]
        pub headline: TemplateChild<gtk::Label>,

        #[template_child]
        pub links_headline: TemplateChild<gtk::Label>,
        #[template_child]
        pub links: TemplateChild<gtk::ListBox>,

        #[template_child]
        pub content: TemplateChild<gtk::Label>,
        #[template_child]
        pub other_names_headline: TemplateChild<gtk::Label>,
        #[template_child]
        pub other_names: TemplateChild<gtk::ListBox>,
        #[template_child]
        pub search_images: TemplateChild<gtk::Button>,

        #[template_child]
        pub content_box: TemplateChild<gtk::Box>,

        #[property(get, set)]
        pub narrow: Cell<bool>,
        pub on_status_page: Cell<bool>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for WikiPage {
        const NAME: &'static str = "WikiPage";
        type Type = super::WikiPage;
        type ParentType = gtk::Box;

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
        }

        // You must call `Widget`'s `init_template()` within `instance_init()`.
        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for WikiPage {
        fn constructed(&self) {
            self.parent_constructed();
        }
    }

    impl WidgetImpl for WikiPage {}
    impl BoxImpl for WikiPage {}
}

glib::wrapper! {
    pub struct WikiPage(ObjectSubclass<imp::WikiPage>)
        @extends gtk::Widget, gtk::Box,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl WikiPage {
    pub fn init(&self, state: &State) {
        self.imp().on_status_page.set(true);

        self.imp()
            .booru_list
            .set_child(Some(&make_booru_selection_list(state, true)));

        let status_page = adw::StatusPage::builder()
            .icon_name("search-global-symbolic")
            .title("Wiki")
            .description("Read Tag descriptions")
            .build();
        self.imp().window.set_child(Some(&status_page));
        self.imp().search_images.set_visible(false);

        let search = self.imp().search.clone();
        self.imp().submit_search.connect_clicked(clone!(
            #[weak]
            search,
            move |_| {
                search.emit_activate();
            }
        ));
        let sender = &state.sender;
        search.connect_activate(clone!(
            #[strong]
            sender,
            move |this| {
                send!(sender, Action::OpenWikiPage(this.text().to_string()));
            }
        ));

        self.bind_property("narrow", &self.imp().split.get(), "collapsed")
            .flags(glib::BindingFlags::SYNC_CREATE)
            .build();

        self.imp().content.connect_activate_link(clone!(
            #[strong]
            sender,
            move |_, url| {
                if let Some(article_title) = url.strip_prefix("wiki:") {
                    send!(sender, Action::OpenWikiPage(article_title.to_string()));
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            }
        ));
    }

    pub fn set_content(&self, entry: &WikiEntry) {
        if self.imp().on_status_page.get() {
            self.imp().on_status_page.set(false);
            self.imp()
                .window
                .set_child(Some(&self.imp().content_box.get()));
        }

        self.imp().headline.set_text(&entry.title);
        self.imp()
            .content
            .set_markup(&Self::parse_content_markup(&entry.body));

        self.imp().other_names.remove_all();
        for name in &entry.other_names {
            let label = gtk::Label::new(Some(&name));
            self.imp().other_names.append(&label);
        }
        self.imp()
            .other_names_headline
            .set_visible(!entry.other_names.is_empty());

        let history_item = &gtk::Button::new();
        history_item.add_css_class("flat");
        history_item.set_label(&entry.title);
        history_item.set_action_name(Some("app.open-wiki-page"));
        history_item.set_action_target_value(Some(&glib::Variant::from(&entry.title)));
        self.imp().wiki_history.insert(history_item, 0);

        self.imp().links.remove_all();
        for link in &entry.links {
            let button = gtk::LinkButton::new(&link);
            self.imp().links.append(&button);
        }
        self.imp()
            .links_headline
            .set_visible(!entry.links.is_empty());

        self.imp().search_images.set_visible(true);
        self.imp()
            .search_images
            .set_action_name(Some("app.open-browse-search"));
        self.imp()
            .search_images
            .set_action_target_value(Some(&glib::Variant::from(&entry.title)));
    }

    // replace
    // "\r" ""
    // "\nh1" hi\n" "<span size="x-large">hi</span>"
    //
    // ‘x-small’, ‘small’, ‘medium’, ‘large’, ‘x-large’
    //
    // "[[text]]" "<a href=\"wiki:text\">text</a>"
    // "[[label|text]]" "<a href=\"wiki:text\">label</a>"
    // "*" "•"
    fn parse_content_markup(content: &str) -> String {
        let re = regex::Regex::new(r"\n\s?h1.([^\n]+)\n").unwrap();
        let content = re.replace_all(
            &content,
            "\n<span weight=\"bold\" size=\"xx-large\">$1</span>\n",
        );
        let re = regex::Regex::new(r"\n\s?h2.([^\n]+)\n").unwrap();
        let content = re.replace_all(
            &content,
            "\n<span weight=\"bold\" size=\"x-large\">$1</span>\n",
        );
        let re = regex::Regex::new(r"\n\s?h3.([^\n]+)\n").unwrap();
        let content = re.replace_all(
            &content,
            "\n<span weight=\"bold\" size=\"large\">$1</span>\n",
        );
        let re = regex::Regex::new(r"\n\s?h4.([^\n]+)\n").unwrap();
        let content = re.replace_all(
            &content,
            "\n<span weight=\"bold\" size=\"medium\">$1</span>\n",
        );

        let re = regex::Regex::new(r"\[\[([^\|\]]+)\|([^\]]+)\]\]").unwrap();
        let content = re.replace_all(&content, "<a href=\"wiki:$1\">$2</a>");

        let re = regex::Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
        let content = re.replace_all(&content, "<a href=\"wiki:$1\">$1</a>");

        let re = regex::Regex::new(r"\*").unwrap();
        let content = re.replace_all(&content, "•");
        content.to_string()
    }

    pub fn update_booru_selection(&self, state: &State) {
        self.imp()
            .booru_list
            .set_child(Some(&make_booru_selection_list(state, true)));
    }
}