├── rari-npm ├── .npmignore ├── .gitignore ├── lib │ ├── index.d.ts │ ├── index.js │ ├── generate-types.js │ ├── cli.js │ └── postinstall.js ├── README.md ├── LICENSE ├── package.json └── LICENSE.MIT ├── tests └── data │ ├── content │ └── files │ │ └── .keep │ └── translated_content │ └── files │ └── .keep ├── crates ├── rari-tools │ ├── src │ │ ├── tests │ │ │ ├── mod.rs │ │ │ └── fixtures │ │ │ │ ├── mod.rs │ │ │ │ ├── sidebars.rs │ │ │ │ ├── redirects.rs │ │ │ │ └── wikihistory.rs │ │ ├── fix │ │ │ ├── mod.rs │ │ │ └── fixer.rs │ │ ├── lib.rs │ │ ├── git.rs │ │ ├── wikihistory.rs │ │ ├── error.rs │ │ └── utils.rs │ └── Cargo.toml ├── rari-utils │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── concat.rs │ │ └── io.rs │ └── Cargo.toml ├── rari-data │ ├── src │ │ ├── lib.rs │ │ └── error.rs │ └── Cargo.toml ├── css-syntax │ ├── .gitignore │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ └── syntax_provider.rs │ ├── build.rs │ └── Cargo.toml ├── rari-doc │ ├── src │ │ ├── sidebars │ │ │ └── mod.rs │ │ ├── pages │ │ │ ├── mod.rs │ │ │ ├── types │ │ │ │ └── mod.rs │ │ │ └── templates.rs │ │ ├── templ │ │ │ ├── templs │ │ │ │ ├── echo.rs │ │ │ │ ├── embeds │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── live_sample_link.rs │ │ │ │ │ ├── embed_youtube.rs │ │ │ │ │ ├── embed_gh_live_sample.rs │ │ │ │ │ ├── jsfiddle_embed.rs │ │ │ │ │ └── interactive_example.rs │ │ │ │ ├── links │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── svgattr.rs │ │ │ │ │ ├── csp.rs │ │ │ │ │ ├── svgxref.rs │ │ │ │ │ ├── mathmlxref.rs │ │ │ │ │ ├── htmlxref.rs │ │ │ │ │ ├── rfc.rs │ │ │ │ │ ├── webextapixref.rs │ │ │ │ │ ├── jsxref.rs │ │ │ │ │ └── domxref.rs │ │ │ │ ├── specification.rs │ │ │ │ ├── glossary.rs │ │ │ │ ├── quick_links_with_subpages.rs │ │ │ │ ├── glossarydisambiguation.rs │ │ │ │ ├── subpages_with_summaries.rs │ │ │ │ ├── inline_labels.rs │ │ │ │ ├── api_list_specs.rs │ │ │ │ ├── webext_all_examples.rs │ │ │ │ ├── js_property_attributes.rs │ │ │ │ ├── api_list_alpha.rs │ │ │ │ ├── cssinfo.rs │ │ │ │ ├── listsubpages.rs │ │ │ │ ├── web_ext_examples.rs │ │ │ │ ├── list_subpages_for_sidebar.rs │ │ │ │ ├── xsltref.rs │ │ │ │ ├── badges.rs │ │ │ │ ├── firefox_for_developers.rs │ │ │ │ ├── mod.rs │ │ │ │ └── banners.rs │ │ │ └── mod.rs │ │ ├── html │ │ │ ├── mod.rs │ │ │ ├── ids.rs │ │ │ └── banner.rs │ │ ├── helpers │ │ │ ├── mod.rs │ │ │ ├── api_inheritance.rs │ │ │ ├── titles.rs │ │ │ ├── parents.rs │ │ │ ├── summary_hack.rs │ │ │ ├── json_data.rs │ │ │ ├── web_ext_examples.rs │ │ │ ├── l10n.rs │ │ │ └── title.rs │ │ ├── find.rs │ │ ├── baseline.rs │ │ ├── walker.rs │ │ ├── lib.rs │ │ └── contributors.rs │ └── Cargo.toml ├── rari-linter │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── rari-templ-func │ ├── README.md │ ├── Cargo.toml │ └── tests │ │ └── basic.rs ├── css-definition-syntax │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── walk.rs │ │ └── tokenizer.rs │ └── Cargo.toml ├── rari-md │ ├── src │ │ ├── ext.rs │ │ ├── anchor.rs │ │ ├── error.rs │ │ ├── ctype.rs │ │ ├── p.rs │ │ ├── dl.rs │ │ └── utils.rs │ ├── README.md │ └── Cargo.toml ├── css-syntax-types │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ └── specs.rs │ └── Cargo.toml ├── rari-deps │ ├── src │ │ ├── lib.rs │ │ ├── current.rs │ │ ├── mdn_data.rs │ │ ├── web_ext_examples.rs │ │ ├── web_features.rs │ │ ├── client.rs │ │ ├── error.rs │ │ ├── external_json.rs │ │ ├── bcd.rs │ │ └── popularities.rs │ ├── test │ │ └── url-titles.json │ └── Cargo.toml ├── rari-lsp │ ├── src │ │ ├── keywords.rs │ │ ├── parser.rs │ │ └── lib.rs │ └── Cargo.toml ├── rari-sitemap │ ├── src │ │ └── ser.rs │ └── Cargo.toml ├── rari-types │ ├── src │ │ ├── error.rs │ │ └── templ.rs │ └── Cargo.toml └── diff-test │ ├── Cargo.toml │ └── src │ └── xml.rs ├── .github ├── release-please-manifest.json ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug.yml ├── workflows │ ├── release-please.yml │ ├── main-review-companion.yml │ ├── build-and-upload-binaries.yml │ ├── publish-npm-manual.yml │ ├── publish-npm.yml │ ├── _test-content.yml │ ├── _test-content-run.yml │ ├── _deploy.yml │ └── build-content-and-diff.yml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml ├── release-please-config.json └── settings.yml ├── .cargo └── config.toml ├── .editorconfig ├── LICENSE ├── .gitignore ├── CODE_OF_CONDUCT.md ├── .lefthook.yml ├── TODO.md ├── REVIEWING.md ├── LICENSE.MIT ├── SECURITY.md ├── LICENSE.BSD-2-Clause └── README.md /rari-npm/.npmignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /tests/data/content/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/translated_content/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rari-npm/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bin 3 | -------------------------------------------------------------------------------- /crates/rari-tools/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | -------------------------------------------------------------------------------- /.github/release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.2.8" 3 | } 4 | -------------------------------------------------------------------------------- /crates/rari-tools/src/fix/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixer; 2 | pub mod issues; 3 | -------------------------------------------------------------------------------- /crates/rari-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod concat; 2 | pub mod error; 3 | pub mod io; 4 | -------------------------------------------------------------------------------- /crates/rari-data/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod baseline; 2 | pub mod error; 3 | pub mod specs; 4 | -------------------------------------------------------------------------------- /crates/css-syntax/.gitignore: -------------------------------------------------------------------------------- 1 | package 2 | webref-css-*.tgz 3 | @webref/ 4 | browser-specs 5 | -------------------------------------------------------------------------------- /crates/css-syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod syntax; 3 | pub mod syntax_provider; 4 | -------------------------------------------------------------------------------- /rari-npm/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const rariBin: string; 2 | export * from "./rari-types.js"; 3 | -------------------------------------------------------------------------------- /crates/rari-doc/src/sidebars/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apiref; 2 | pub mod default_api_sidebar; 3 | pub mod jsref; 4 | -------------------------------------------------------------------------------- /crates/rari-linter/src/lib.rs: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // - filepath checks 3 | // - check *.md for merge conflicts 4 | -------------------------------------------------------------------------------- /crates/rari-doc/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod build; 2 | pub mod json; 3 | pub mod page; 4 | pub mod templates; 5 | pub mod types; 6 | -------------------------------------------------------------------------------- /crates/rari-tools/src/tests/fixtures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod docs; 2 | pub mod redirects; 3 | pub mod sidebars; 4 | pub mod wikihistory; 5 | -------------------------------------------------------------------------------- /crates/rari-templ-func/README.md: -------------------------------------------------------------------------------- 1 | # rari-templ-func 2 | 3 | This library provides a convenient derive macro for [rari-templ](../rari-doc/src/templ/) functions. 4 | -------------------------------------------------------------------------------- /crates/css-definition-syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod generate; 3 | pub mod parser; 4 | pub mod tokenizer; 5 | pub mod walk; 6 | 7 | pub use generate::generate; 8 | -------------------------------------------------------------------------------- /rari-npm/README.md: -------------------------------------------------------------------------------- 1 | # rari on npm 2 | 3 | > [!WARNING] 4 | > This is still experimental and work in progress. 5 | 6 | This exposes [rari](https://github.com/mdn/rari) in the npm world. 7 | -------------------------------------------------------------------------------- /rari-npm/lib/index.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | export const rariBin = join( 4 | import.meta.dirname, 5 | `../bin/rari${process.platform === "win32" ? ".exe" : ""}`, 6 | ); 7 | -------------------------------------------------------------------------------- /crates/rari-doc/src/pages/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blog; 2 | pub mod contributors; 3 | pub mod curriculum; 4 | pub mod doc; 5 | pub mod generic; 6 | pub mod spa; 7 | pub mod spa_homepage; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /crates/rari-md/src/ext.rs: -------------------------------------------------------------------------------- 1 | pub static DELIM_START: &str = "⟬"; 2 | pub static DELIM_START_LEN: usize = DELIM_START.len(); 3 | pub static DELIM_END: &str = "⟭"; 4 | pub static DELIM_END_LEN: usize = DELIM_END.len(); 5 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/echo.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | 5 | #[rari_f(register = "crate::Templ")] 6 | pub fn echo(s: String) -> Result { 7 | Ok(s) 8 | } 9 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod embed_gh_live_sample; 2 | pub mod embed_live_sample; 3 | pub mod embed_youtube; 4 | pub mod interactive_example; 5 | pub mod jsfiddle_embed; 6 | pub mod live_sample_link; 7 | -------------------------------------------------------------------------------- /crates/rari-doc/src/html/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod banner; 2 | pub mod bubble_up; 3 | pub mod code; 4 | mod fix_img; 5 | mod fix_link; 6 | pub mod ids; 7 | pub mod links; 8 | pub mod modifier; 9 | pub mod rewriter; 10 | pub mod sections; 11 | pub mod sidebar; 12 | -------------------------------------------------------------------------------- /rari-npm/LICENSE: -------------------------------------------------------------------------------- 1 | This project is licensed under the Mozilla Public License 2.0. 2 | See LICENSE.MPL-2.0 file for more information. 3 | 4 | This project includes code from vscode-ripgrep licensed under the MIT License. 5 | See LICENSE.MIT file for more information. 6 | -------------------------------------------------------------------------------- /crates/rari-linter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-linter" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | thiserror.workspace = true 11 | -------------------------------------------------------------------------------- /crates/rari-utils/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | #[error("io error: {source} ({path})")] 7 | pub struct RariIoError { 8 | pub path: PathBuf, 9 | pub source: std::io::Error, 10 | } 11 | -------------------------------------------------------------------------------- /crates/rari-data/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error(transparent)] 6 | IoError(#[from] rari_utils::error::RariIoError), 7 | #[error(transparent)] 8 | JsonError(#[from] serde_json::Error), 9 | } 10 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod csp; 2 | pub mod cssxref; 3 | pub mod domxref; 4 | pub mod htmlxref; 5 | pub mod http; 6 | pub mod jsxref; 7 | pub mod mathmlxref; 8 | pub mod rfc; 9 | pub mod svgattr; 10 | pub mod svgxref; 11 | pub mod webextapixref; 12 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | TESTING_CONTENT_ROOT = { value = "tests/data/content/files", relative = true } 3 | TESTING_CONTENT_TRANSLATED_ROOT = { value = "tests/data/translated_content/files", relative = true } 4 | TESTING_CACHE_CONTENT = "0" 5 | TESTING_READER_IGNORES_GITIGNORE = "1" 6 | -------------------------------------------------------------------------------- /crates/css-definition-syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "css-definition-syntax" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | thiserror.workspace = true 11 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_inheritance; 2 | pub mod css_info; 3 | pub mod json_data; 4 | pub mod l10n; 5 | pub mod parents; 6 | pub mod subpages; 7 | pub mod summary_hack; 8 | pub mod title; 9 | pub mod titles; 10 | pub mod web_ext_examples; 11 | pub mod webextapi; 12 | -------------------------------------------------------------------------------- /crates/css-syntax-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod css; 2 | pub mod error; 3 | pub mod interfaces; 4 | pub mod specs; 5 | pub mod utils; 6 | pub mod values; 7 | 8 | pub use css::*; 9 | pub use error::*; 10 | pub use interfaces::*; 11 | pub use specs::*; 12 | pub use utils::*; 13 | pub use values::*; 14 | -------------------------------------------------------------------------------- /crates/rari-utils/src/concat.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! concat_strs { 3 | ($($s:expr),+) => {{ 4 | let mut len = 0; 5 | $(len += $s.len();)+ 6 | let mut out = String::with_capacity(len); 7 | $(out.push_str($s);)+ 8 | out 9 | }} 10 | } 11 | -------------------------------------------------------------------------------- /crates/rari-deps/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bcd; 2 | pub mod client; 3 | pub mod current; 4 | pub mod error; 5 | pub mod external_json; 6 | pub mod github_release; 7 | pub mod mdn_data; 8 | pub mod npm; 9 | pub mod popularities; 10 | pub mod web_ext_examples; 11 | pub mod web_features; 12 | pub mod webref_css; 13 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Module Overview 2 | //! 3 | //! This module serves as the entry point for various submodules that handle different aspects 4 | //! of the parsing and rendering. 5 | 6 | pub mod api; 7 | pub mod legacy; 8 | pub mod parser; 9 | pub mod render; 10 | pub mod templs; 11 | -------------------------------------------------------------------------------- /crates/rari-lsp/src/keywords.rs: -------------------------------------------------------------------------------- 1 | use rari_doc::Templ; 2 | use rari_doc::templ::templs::TEMPL_MAP; 3 | 4 | pub(crate) type KeywordDocsMap = std::collections::HashMap<&'static str, &'static Templ>; 5 | 6 | pub(crate) fn load_kw_docs() -> KeywordDocsMap { 7 | TEMPL_MAP.iter().map(|t| (t.name, *t)).collect() 8 | } 9 | -------------------------------------------------------------------------------- /crates/rari-deps/src/current.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use semver::Version; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Deserialize, Serialize, Default, Debug)] 6 | pub struct Current { 7 | pub latest_last_check: Option>, 8 | pub current_version: Option, 9 | } 10 | -------------------------------------------------------------------------------- /crates/rari-deps/src/mdn_data.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use rari_types::globals::deps; 4 | 5 | use crate::error::DepsError; 6 | use crate::npm::get_package; 7 | 8 | pub fn update_mdn_data(base_path: &Path) -> Result<(), DepsError> { 9 | get_package("mdn-data", &deps().mdn_data, base_path)?; 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /crates/rari-sitemap/src/ser.rs: -------------------------------------------------------------------------------- 1 | use rari_types::globals::base_url; 2 | use rari_utils::concat_strs; 3 | use serde::Serializer; 4 | 5 | pub(crate) fn prefix_base_url(loc: &str, serializer: S) -> Result 6 | where 7 | S: Serializer, 8 | { 9 | serializer.serialize_str(&concat_strs!(base_url(), loc)) 10 | } 11 | -------------------------------------------------------------------------------- /crates/rari-lsp/src/parser.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn initialise_parser() -> tree_sitter::Parser { 2 | let mut parser = tree_sitter::Parser::new(); 3 | if parser 4 | .set_language(&tree_sitter_mdn::LANGUAGE.into()) 5 | .is_err() 6 | { 7 | panic!("Failed to set parser language"); 8 | } 9 | parser 10 | } 11 | -------------------------------------------------------------------------------- /crates/css-syntax/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | fn main() -> Result<(), Error> { 4 | #[cfg(not(feature = "rari"))] 5 | { 6 | let package_path = std::path::Path::new("./"); 7 | rari_deps::webref_css::update_webref_css(package_path)?; 8 | } 9 | println!("cargo::rerun-if-changed=build.rs"); 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /crates/rari-tools/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod add_redirect; 2 | pub mod error; 3 | pub mod fix; 4 | pub mod git; 5 | pub mod history; 6 | pub mod inventory; 7 | pub mod r#move; 8 | pub mod redirects; 9 | pub mod remove; 10 | pub mod sidebars; 11 | pub mod sync_translated_content; 12 | #[cfg(test)] 13 | pub mod tests; 14 | mod utils; 15 | pub mod wikihistory; 16 | -------------------------------------------------------------------------------- /crates/rari-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-utils" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | thiserror.workspace = true 11 | serde.workspace = true 12 | serde_json.workspace = true 13 | tracing.workspace = true 14 | -------------------------------------------------------------------------------- /crates/rari-utils/src/io.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | use crate::error::RariIoError; 5 | 6 | pub fn read_to_string>(path: P) -> Result { 7 | fs::read_to_string(path.as_ref()).map_err(|e| RariIoError { 8 | source: e, 9 | path: path.as_ref().to_path_buf(), 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /crates/css-syntax-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "css-syntax-types" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | serde_json.workspace = true 11 | serde.workspace = true 12 | url.workspace = true 13 | regress.workspace = true 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.rs] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is licensed under the Mozilla Public License 2.0. 2 | See LICENSE.MPL-2.0 file for more information. 3 | 4 | This project includes code from Comrak licensed under the BSD 2-Clause License. 5 | See LICENSE.BSD-2-Clause file for more information. 6 | 7 | This project includes code from vscode-ripgrep licensed under the MIT License. 8 | See LICENSE.MIT file for more information. 9 | -------------------------------------------------------------------------------- /crates/rari-md/README.md: -------------------------------------------------------------------------------- 1 | To update `html.rs` when upgrading comrak: 2 | 3 | ```sh 4 | export FROM=from-version 5 | export TO=to-version 6 | curl -o /tmp/html.rs.${FROM} https://github.com/kivikakk/comrak/raw/refs/tags/v${FROM}/src/html.rs 7 | curl -o /tmp/html.rs.${TO} https://github.com/kivikakk/comrak/raw/refs/tags/v${TO}/src/html.rs 8 | git merge-file src/html.rs /tmp/html.rs.${FROM} /tmp/html.rs.${TO} 9 | ``` 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .config.toml 3 | .DS_Store 4 | tests/data/content/files/* 5 | !tests/data/content/files/.keep 6 | tests/data/translated_content/files/* 7 | !tests/data/translated_content/files/.keep 8 | 9 | # might get created from debugging 10 | /package 11 | 12 | # Must be top level so these are included in npm publish. 13 | rari-npm/schema.json 14 | rari-npm/lib/rari-types.d.ts 15 | 16 | # inline deps 17 | /.deps 18 | -------------------------------------------------------------------------------- /crates/rari-deps/src/web_ext_examples.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::error::DepsError; 4 | use crate::external_json::get_json; 5 | 6 | pub fn update_web_ext_examples(base_path: &Path) -> Result<(), DepsError> { 7 | get_json( 8 | "web_ext_examples", 9 | "https://raw.githubusercontent.com/mdn/webextensions-examples/main/examples.json", 10 | base_path, 11 | )?; 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /crates/rari-types/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, PartialEq, Clone, Copy, Error)] 4 | pub enum EnvError { 5 | #[error("CONTENT_ROOT must be set")] 6 | NoContent, 7 | #[error("CONTENT_TRANSLATED_ROOT must be set")] 8 | NoTranslatedContent, 9 | #[error("BUILD_OUT_ROOT must be set")] 10 | NoBuildOut, 11 | #[error("CONTRIBUTOR_SPOTLIGHT_ROOT must be set")] 12 | NoContributorSpotlightRoot, 13 | } 14 | -------------------------------------------------------------------------------- /crates/rari-md/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-md" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | anyhow.workspace = true 11 | regex.workspace = true 12 | thiserror.workspace = true 13 | rari-types.workspace = true 14 | itertools.workspace = true 15 | base64.workspace = true 16 | 17 | comrak = { version = "0.39", default-features = false } 18 | -------------------------------------------------------------------------------- /crates/rari-templ-func/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-templ-func" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | rari-types.workspace = true 14 | darling.workspace = true 15 | quote.workspace = true 16 | 17 | syn = { version = "2", features = ["full"] } 18 | 19 | [dev-dependencies] 20 | anyhow.workspace = true 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Content or feature request 4 | url: https://github.com/mdn/mdn/issues/new/choose 5 | about: Propose new content for MDN Web Docs or submit a feature request using this link. 6 | - name: MDN GitHub Discussions 7 | url: https://github.com/orgs/mdn/discussions 8 | about: Does the issue involve a lot of changes, or is it hard to split it into actionable tasks? Start a discussion before opening an issue. 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, read [Mozilla's Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 5 | 6 | ## Reporting violations 7 | 8 | For more information on how to report violations of the Community Participation Guidelines, read the [How to report](https://www.mozilla.org/about/governance/policies/participation/reporting/) page. 9 | -------------------------------------------------------------------------------- /crates/rari-data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-data" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rari-utils.workspace = true 11 | thiserror.workspace = true 12 | serde.workspace = true 13 | serde_json.workspace = true 14 | url.workspace = true 15 | chrono.workspace = true 16 | indexmap.workspace = true 17 | schemars.workspace = true 18 | tracing.workspace = true 19 | -------------------------------------------------------------------------------- /crates/rari-sitemap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-sitemap" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rari-doc.workspace = true 11 | rari-types.workspace = true 12 | rari-utils.workspace = true 13 | thiserror.workspace = true 14 | serde.workspace = true 15 | chrono.workspace = true 16 | 17 | quick-xml = { version = "0.37", features = ["serialize"] } 18 | flate2 = "1" 19 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/specification.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | 5 | #[rari_f(register = "crate::Templ")] 6 | pub fn specifications() -> Result { 7 | let queries = env.browser_compat.join(","); 8 | let specs = env.spec_urls.join(","); 9 | Ok(format!( 10 | r#"
11 | If you're able to see this, something went wrong on this page. 12 |
"# 13 | )) 14 | } 15 | -------------------------------------------------------------------------------- /rari-npm/lib/generate-types.js: -------------------------------------------------------------------------------- 1 | import { compileFromFile } from 'json-schema-to-typescript' 2 | import { dirname, join } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import fs from 'node:fs'; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | const schemaPath = join(__dirname, '..', 'schema.json'); 8 | const typesPath = join(__dirname, 'rari-types.d.ts'); 9 | 10 | compileFromFile(schemaPath, { 11 | additionalProperties: false, 12 | }).then(ts => fs.writeFileSync(typesPath, ts)) 13 | 14 | -------------------------------------------------------------------------------- /crates/rari-deps/src/web_features.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use rari_types::globals::deps; 4 | 5 | use crate::error::DepsError; 6 | use crate::github_release::get_artifact; 7 | 8 | pub fn update_web_features(base_path: &Path) -> Result<(), DepsError> { 9 | //get_package("web-features", None, base_path)?; 10 | 11 | get_artifact( 12 | "web-platform-dx/web-features", 13 | "data.extended.json", 14 | "baseline", 15 | &deps().web_features, 16 | base_path, 17 | )?; 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/api_inheritance.rs: -------------------------------------------------------------------------------- 1 | use super::json_data::json_data_interface; 2 | 3 | pub fn inheritance(main_if: &str) -> Vec<&str> { 4 | let web_api_data = json_data_interface(); 5 | let mut inherited = vec![]; 6 | 7 | let mut interface = main_if; 8 | while let Some(inherited_data) = web_api_data 9 | .get(interface) 10 | .map(|data| data.inh.as_str()) 11 | .filter(|ihn| !ihn.is_empty()) 12 | { 13 | inherited.push(inherited_data); 14 | interface = inherited_data; 15 | } 16 | inherited 17 | } 18 | -------------------------------------------------------------------------------- /.lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | fmt: 5 | glob: "*.rs" 6 | run: cargo fmt --all 7 | stage_fixed: true 8 | 9 | clippy: 10 | glob: "*.rs" 11 | run: cargo clippy --all-features --workspace -- -Dwarnings 12 | 13 | pre-push: 14 | parallel: true 15 | commands: 16 | test: 17 | run: cargo test --verbose --workspace --all-targets --no-fail-fast 18 | 19 | test-doc: 20 | run: cargo test --verbose --workspace --doc --features doctest --no-fail-fast 21 | 22 | output: 23 | - summary 24 | - failure 25 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: read 8 | 9 | name: release-please 10 | 11 | jobs: 12 | release-please: 13 | if: github.repository == 'mdn/rari' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 17 | with: 18 | config-file: .github/release-please-config.json 19 | manifest-file: .github/release-please-manifest.json 20 | token: ${{ secrets.RELEASE_PLEASE_GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /crates/rari-deps/test/url-titles.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://drafts.csswg.org/css-color-5/": "CSS Color Module Level 5", 3 | "https://drafts.csswg.org/css-conditional-5/": "CSS Conditional Rules Module Level 5", 4 | "https://drafts.csswg.org/css-images-3/": "CSS Images Module Level 3", 5 | "https://drafts.csswg.org/css-backgrounds-3/": "CSS Backgrounds and Borders Module Level 3", 6 | "https://drafts.csswg.org/css-borders-4/": "CSS Borders and Box Decorations Module Level 4", 7 | "https://drafts.csswg.org/css-speech-1/": "CSS Speech Module Level 1", 8 | "https://drafts.csswg.org/selectors-4/": "Selectors Level 4" 9 | } 10 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO's 2 | 3 | ## rari 4 | - [x] Change compat macro to no args only and refactor `data-multiple` 5 | - [ ] browser_compat sections should be able to contain content in yari ?! 6 | - [x] unify prev next ... 7 | - [ ] short_title falls back to title?! 8 | - [ ] base url?! 9 | - [ ] blog order uses inverse title compare as fallback 10 | - [ ] unify link rendering 11 | - [x] locale compare for sidebar?! 12 | - [x] l10n for jsref 13 | - [x] deal with summary() in templs 14 | - [ ] list groups badges and titles 15 | - [ ] WebKitCSSMatrix title 16 | 17 | ## translated content 18 | 19 | - [ ] lower case codefences attributes 20 | -------------------------------------------------------------------------------- /crates/rari-lsp/src/lib.rs: -------------------------------------------------------------------------------- 1 | use tower_lsp_server::{LspService, Server}; 2 | 3 | mod keywords; 4 | mod lsp; 5 | mod parser; 6 | mod position; 7 | 8 | pub fn run() -> Result<(), anyhow::Error> { 9 | tokio::runtime::Builder::new_current_thread() 10 | .enable_all() 11 | .build() 12 | .unwrap() 13 | .block_on(async { 14 | let (stdin, stdout) = (tokio::io::stdin(), tokio::io::stdout()); 15 | 16 | let (service, socket) = LspService::build(lsp::Backend::new).finish(); 17 | Server::new(stdin, stdout, socket).serve(service).await; 18 | }); 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/css-syntax/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use css_definition_syntax::error::SyntaxDefinitionError; 4 | use css_definition_syntax::parser::Node; 5 | use thiserror::Error; 6 | 7 | #[derive(Debug, Clone, Error)] 8 | pub enum SyntaxError { 9 | #[error(transparent)] 10 | SyntaxDefinitionError(#[from] SyntaxDefinitionError), 11 | #[error("Expected group node, got: {}", .0.str_name())] 12 | ExpectedGroupNode(Node), 13 | #[error("IoError")] 14 | IoError, 15 | #[error("fmtError")] 16 | FmtError(#[from] fmt::Error), 17 | #[error("Error: could not find syntax for this item")] 18 | NoSyntaxFound, 19 | } 20 | -------------------------------------------------------------------------------- /crates/css-definition-syntax/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::parser::Node; 4 | 5 | #[derive(Debug, Clone, Error)] 6 | pub enum SyntaxDefinitionError { 7 | #[error("Expected Range node")] 8 | ExpectedRangeNode, 9 | #[error("Unknown node type {0:?}")] 10 | UnknownNodeType(Node), 11 | #[error("Parse error: Expected {0}")] 12 | ParseErrorExpected(char), 13 | #[error("Parse error: Expected function")] 14 | ParseErrorExpectedFunction, 15 | #[error("Parse error: Expected keyword")] 16 | ParseErrorExpectedKeyword, 17 | #[error("Parse error: Unexpected input")] 18 | ParseErrorUnexpectedInput, 19 | } 20 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/glossary.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::templ::api::RariApi; 5 | use crate::utils::dedup_whitespace; 6 | 7 | #[rari_f(register = "crate::Templ")] 8 | pub fn glossary(term_name: String, display_name: Option) -> Result { 9 | let url = format!( 10 | "/Glossary/{}", 11 | dedup_whitespace(&term_name).replace(' ', "_") 12 | ); 13 | RariApi::link( 14 | &url, 15 | Some(env.locale), 16 | Some(&display_name.unwrap_or(term_name)), 17 | false, 18 | None, 19 | false, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /crates/rari-deps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-deps" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rari-utils.workspace = true 11 | rari-types.workspace = true 12 | serde.workspace = true 13 | serde_json.workspace = true 14 | chrono.workspace = true 15 | thiserror.workspace = true 16 | reqwest.workspace = true 17 | url.workspace = true 18 | indexmap.workspace = true 19 | tracing.workspace = true 20 | semver.workspace = true 21 | 22 | css-syntax-types = { path = "../css-syntax-types" } 23 | tar = "0.4" 24 | flate2 = "1" 25 | csv = "1" 26 | -------------------------------------------------------------------------------- /crates/diff-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diff-test" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rayon.workspace = true 11 | anyhow.workspace = true 12 | ignore.workspace = true 13 | itertools.workspace = true 14 | regex.workspace = true 15 | serde.workspace = true 16 | serde_json.workspace = true 17 | base64.workspace = true 18 | 19 | jsonpath_lib = "0.3" 20 | prettydiff = "0.9" 21 | html-minifier = "5" 22 | ansi-to-html = "0.2" 23 | similar = "2" 24 | quick-xml = "0.37" 25 | clap = { version = "4", features = ["derive"] } 26 | lol_html = "2" 27 | -------------------------------------------------------------------------------- /rari-npm/lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from "node:child_process"; 4 | import process from "node:process"; 5 | import { rariBin } from "./index.js"; 6 | 7 | const input = process.argv.slice(2); 8 | 9 | spawn(rariBin, input, { stdio: "inherit" }).on("exit", (code, signal) => { 10 | if (signal) { 11 | try { 12 | process.kill(process.pid, signal); 13 | } catch { 14 | // Reflect signal code in exit code. 15 | // See: https://nodejs.org/api/os.html#os-constants 16 | const signalCode = os.constants?.signals?.[signal] ?? 0; 17 | process.exit(128 + signalCode); 18 | } 19 | } 20 | process.exit(code ?? 0); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # MDN Rari CODEOWNERS 3 | # ---------------------------------------------------------------------------- 4 | # Order is important. The last matching pattern takes precedence. 5 | # For more detailed information, see: 6 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 7 | # ---------------------------------------------------------------------------- 8 | 9 | * @mdn/engineering 10 | 11 | /Cargo.lock @mdn/engineering @mdn-bot 12 | /Cargo.toml @mdn/engineering @mdn-bot 13 | /SECURITY.md @mdn/engineering 14 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/quick_links_with_subpages.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::{AnyArg, Arg}; 3 | 4 | use super::listsubpages::listsubpages; 5 | use crate::error::DocError; 6 | use crate::templ::legacy::fix_broken_legacy_url; 7 | 8 | /// List sub pages 9 | #[rari_f(register = "crate::Templ")] 10 | pub fn quicklinkswithsubpages(url: Option) -> Result { 11 | let url = url.map(|s| fix_broken_legacy_url(&s, env.locale).to_string()); 12 | listsubpages( 13 | env, 14 | url, 15 | Some(AnyArg { value: Arg::Int(2) }), 16 | None, 17 | Some(AnyArg { 18 | value: Arg::Bool(true), 19 | }), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### Motivation 8 | 9 | 10 | 11 | ### Additional details 12 | 13 | 14 | 15 | ### Related issues and pull requests 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /crates/rari-tools/src/fix/fixer.rs: -------------------------------------------------------------------------------- 1 | use rari_doc::pages::page::{Page, PageLike}; 2 | use rari_types::locale::Locale; 3 | use rayon::iter::{IntoParallelIterator, ParallelIterator}; 4 | 5 | use super::issues::fix_page; 6 | use crate::error::ToolError; 7 | 8 | pub fn fix_all(docs: &[Page], locale: Option) -> Result, ToolError> { 9 | docs.into_par_iter() 10 | .filter(|page| locale.map(|locale| page.locale() == locale).unwrap_or(true)) 11 | .map(|page| (fix_page(page), page)) 12 | .filter_map(|(res, page)| { 13 | if matches!(res, Ok(false)) { 14 | None 15 | } else { 16 | Some(res.map(|_| page)) 17 | } 18 | }) 19 | .collect() 20 | } 21 | -------------------------------------------------------------------------------- /crates/rari-deps/src/client.rs: -------------------------------------------------------------------------------- 1 | use reqwest::blocking::Response; 2 | use url::Url; 3 | 4 | use crate::error::DepsError; 5 | 6 | pub fn get(url: impl AsRef) -> Result { 7 | let url = Url::parse(url.as_ref())?; 8 | let mut req_builder = reqwest::blocking::ClientBuilder::new() 9 | .user_agent("mdn/rari") 10 | .build()? 11 | .get(url.as_ref()); 12 | 13 | // check if the URL's host is api.github.com 14 | if url.host_str() == Some("api.github.com") { 15 | // get the GitHub token from the environment 16 | if let Ok(token) = std::env::var("GITHUB_TOKEN") { 17 | req_builder = req_builder.bearer_auth(token); 18 | } 19 | } 20 | 21 | Ok(req_builder.send()?) 22 | } 23 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/titles.rs: -------------------------------------------------------------------------------- 1 | use rari_types::fm_types::PageType; 2 | 3 | use crate::pages::page::{Page, PageLike}; 4 | 5 | pub fn api_page_title(page: &Page) -> &str { 6 | if let Some(short_title) = page.short_title() { 7 | return short_title; 8 | } 9 | let title = page.title(); 10 | let title = &title[title.rfind('.').map(|i| i + 1).unwrap_or(0)..]; 11 | if matches!(page.page_type(), PageType::WebApiEvent) { 12 | let title = page.slug(); 13 | let title = &title[title.rfind('/').map(|i| i + 1).unwrap_or(0)..]; 14 | if let Some(title) = title.strip_suffix("_event") { 15 | title 16 | } else { 17 | title 18 | } 19 | } else { 20 | title 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/rari-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-types" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [features] 10 | testing = [] 11 | default = [] 12 | 13 | [dependencies] 14 | thiserror.workspace = true 15 | serde.workspace = true 16 | serde_json.workspace = true 17 | indexmap.workspace = true 18 | chrono.workspace = true 19 | schemars.workspace = true 20 | semver.workspace = true 21 | tracing.workspace = true 22 | strum.workspace = true 23 | darling.workspace = true 24 | quote.workspace = true 25 | 26 | serde_variant = "0.1" 27 | normalize-path = "0.2" 28 | dirs = "6" 29 | config = { version = "0.15", default-features = false, features = ["toml"] } 30 | proc-macro2 = "1" 31 | -------------------------------------------------------------------------------- /crates/rari-lsp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-lsp" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rari-doc.workspace = true 11 | rari-tools.workspace = true 12 | rari-types.workspace = true 13 | 14 | serde.workspace = true 15 | serde_json.workspace = true 16 | tokio = { workspace = true, features = ["io-std"] } 17 | tracing.workspace = true 18 | tracing-subscriber.workspace = true 19 | anyhow.workspace = true 20 | tree-sitter.workspace = true 21 | tree-sitter-mdn.workspace = true 22 | 23 | tree-sitter-md = { version = "0.3", features = ["parser"] } 24 | lsp-textdocument = "0.4" 25 | tower-lsp-server = "0.22" 26 | streaming-iterator = "0.1" 27 | dashmap = "6" 28 | -------------------------------------------------------------------------------- /crates/diff-test/src/xml.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use quick_xml::events::Event; 4 | use quick_xml::reader::Reader; 5 | use quick_xml::writer::Writer; 6 | 7 | pub fn fmt_html(html: &str) -> String { 8 | let mut reader = Reader::from_str(html); 9 | reader.config_mut().trim_text(true); 10 | let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 0); 11 | loop { 12 | match reader.read_event() { 13 | Ok(Event::Eof) => break, 14 | // we can either move or borrow the event to write, depending on your use-case 15 | Ok(e) => assert!(writer.write_event(e).is_ok()), 16 | _ => {} 17 | } 18 | } 19 | 20 | let result = writer.into_inner().into_inner(); 21 | String::from_utf8(result).unwrap() 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/main-review-companion.yml: -------------------------------------------------------------------------------- 1 | name: Main Review Companion 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | # Authenticate with GCP. 14 | id-token: write 15 | 16 | jobs: 17 | build: 18 | if: | 19 | github.repository_owner == 'mdn' && 20 | (github.event_name == 'schedule' || github.secret_source == 'Actions') 21 | uses: ./.github/workflows/_build.yml 22 | secrets: inherit 23 | with: 24 | partial: false 25 | 26 | deploy: 27 | needs: build 28 | uses: ./.github/workflows/_deploy.yml 29 | secrets: inherit 30 | with: 31 | prefix: rari 32 | build-artifact-name: ${{ needs.build.outputs.artifact-name }} 33 | -------------------------------------------------------------------------------- /crates/css-syntax-types/src/error.rs: -------------------------------------------------------------------------------- 1 | pub struct ConversionError(std::borrow::Cow<'static, str>); 2 | 3 | impl std::error::Error for ConversionError {} 4 | impl std::fmt::Display for ConversionError { 5 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 6 | std::fmt::Display::fmt(&self.0, f) 7 | } 8 | } 9 | impl std::fmt::Debug for ConversionError { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 11 | std::fmt::Debug::fmt(&self.0, f) 12 | } 13 | } 14 | impl From<&'static str> for ConversionError { 15 | fn from(value: &'static str) -> Self { 16 | Self(value.into()) 17 | } 18 | } 19 | impl From for ConversionError { 20 | fn from(value: String) -> Self { 21 | Self(value.into()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/rari-md/src/anchor.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::sync::LazyLock; 3 | 4 | use regex::Regex; 5 | 6 | pub fn anchorize(content: &str) -> Cow<'_, str> { 7 | static REJECTED_CHARS: LazyLock = 8 | LazyLock::new(|| Regex::new(r#"[*<>"$#%&+,/:;=?@\[\]^`{|}~')(\\]"#).unwrap()); 9 | 10 | let id = REJECTED_CHARS.replace_all(content, ""); 11 | let mut id = id.trim().to_lowercase(); 12 | let mut prev = ' '; 13 | id.retain(|c| { 14 | let result = !c.is_ascii_whitespace() || !prev.is_ascii_whitespace(); 15 | prev = c; 16 | result 17 | }); 18 | let id = id.replace(' ', "_"); 19 | if !id.is_empty() { 20 | if id == content { 21 | Cow::Borrowed(content) 22 | } else { 23 | Cow::Owned(id) 24 | } 25 | } else { 26 | Cow::Borrowed("sect") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/rari-types/src/templ.rs: -------------------------------------------------------------------------------- 1 | use darling::FromMeta; 2 | use proc_macro2::TokenStream; 3 | use quote::{ToTokens, quote}; 4 | 5 | use crate::{Arg, RariEnv}; 6 | 7 | pub type RariFn = fn(&RariEnv<'_>, Vec>) -> R; 8 | 9 | #[derive(Debug, Default, FromMeta, Clone, Copy, strum::Display)] 10 | pub enum TemplType { 11 | #[default] 12 | None, 13 | Link, 14 | Sidebar, 15 | Banner, 16 | } 17 | 18 | impl ToTokens for TemplType { 19 | fn to_tokens(&self, tokens: &mut TokenStream) { 20 | let variant = match self { 21 | TemplType::None => quote! { TemplType::None }, 22 | TemplType::Link => quote! { TemplType::Link }, 23 | TemplType::Sidebar => quote! { TemplType::Sidebar }, 24 | TemplType::Banner => quote! { TemplType::Banner }, 25 | }; 26 | tokens.extend(variant); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | commit-message: 8 | prefix: chore 9 | include: scope 10 | 11 | - package-ecosystem: github-actions 12 | directory: / 13 | schedule: 14 | interval: weekly 15 | commit-message: 16 | prefix: ci 17 | include: scope 18 | 19 | - package-ecosystem: npm 20 | directory: /rari-npm 21 | schedule: 22 | interval: weekly 23 | cooldown: 24 | default-days: 3 25 | groups: 26 | npm-prod: 27 | dependency-type: production 28 | update-types: 29 | - minor 30 | - patch 31 | npm-dev: 32 | dependency-type: development 33 | update-types: 34 | - minor 35 | - patch 36 | commit-message: 37 | prefix: chore 38 | include: scope 39 | -------------------------------------------------------------------------------- /crates/css-syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "css-syntax" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | thiserror.workspace = true 11 | regress.workspace = true 12 | serde_json.workspace = true 13 | serde.workspace = true 14 | url.workspace = true 15 | html-escape.workspace = true 16 | itertools.workspace = true 17 | rari-types = { workspace = true, optional = true } 18 | rari-deps = { workspace = true, optional = true } 19 | 20 | css-syntax-types = { path = "../css-syntax-types" } 21 | css-definition-syntax = { path = "../css-definition-syntax" } 22 | 23 | [build-dependencies] 24 | rari-deps.workspace = true 25 | anyhow = "1" 26 | 27 | [dev-dependencies] 28 | rari-deps.workspace = true 29 | 30 | [features] 31 | doctest = ["dep:rari-deps"] 32 | default = [] 33 | rari = ["dep:rari-types"] 34 | -------------------------------------------------------------------------------- /rari-npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mdn/rari", 3 | "version": "0.2.8", 4 | "description": "npm package for rari", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mdn/rari" 11 | }, 12 | "homepage": "https://github.com/mdn/rari/tree/main/rari-npm", 13 | "scripts": { 14 | "postinstall": "node ./lib/postinstall.js", 15 | "export-schema": "node ./lib/cli.js export-schema", 16 | "generate-types": "node ./lib/generate-types.js" 17 | }, 18 | "bin": { 19 | "rari": "lib/cli.js" 20 | }, 21 | "author": "MDN Engineering Team ", 22 | "license": "MPL-2.0", 23 | "engines": { 24 | "node": ">=20" 25 | }, 26 | "dependencies": { 27 | "extract-zip": "^2.0.1", 28 | "https-proxy-agent": "^7.0.2", 29 | "proxy-from-env": "^1.1.0", 30 | "tar": "^7.4.3", 31 | "json-schema-to-typescript": "^15.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/rari-doc/src/find.rs: -------------------------------------------------------------------------------- 1 | use rari_types::locale::Locale; 2 | 3 | use crate::cached_readers::{STATIC_DOC_PAGE_FILES, STATIC_DOC_PAGE_TRANSLATED_FILES}; 4 | use crate::error::DocError; 5 | use crate::pages::page::Page; 6 | 7 | pub fn doc_pages_from_slugish(slugish: &str, locale: Locale) -> Result, DocError> { 8 | let cache = if locale == Locale::EnUs { 9 | &STATIC_DOC_PAGE_FILES 10 | } else { 11 | &STATIC_DOC_PAGE_TRANSLATED_FILES 12 | }; 13 | cache.get().map_or_else( 14 | || Err(DocError::FileCacheBroken), 15 | |static_files| { 16 | Ok(static_files 17 | .iter() 18 | .filter_map(|((l, s), v)| { 19 | if locale == *l && s.contains(slugish) { 20 | Some(v) 21 | } else { 22 | None 23 | } 24 | }) 25 | .take(100) 26 | .cloned() 27 | .collect()) 28 | }, 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /REVIEWING.md: -------------------------------------------------------------------------------- 1 | # Reviewing guide 2 | 3 | All reviewers must abide by the [code of conduct](CODE_OF_CONDUCT.md); they are also protected by the code of conduct. 4 | A reviewer should not tolerate poor behavior and is encouraged to [report any behavior](CODE_OF_CONDUCT.md#Reporting_violations) that violates the code of conduct. 5 | 6 | ## Review process 7 | 8 | The MDN Web Docs team has a well-defined review process that must be followed by reviewers in all repositories under the GitHub MDN organization. 9 | This process is described in detail on the [Pull request guidelines](https://developer.mozilla.org/en-US/docs/MDN/Community/Pull_requests) page. 10 | 11 | 18 | -------------------------------------------------------------------------------- /crates/rari-doc/src/html/ids.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashSet; 3 | 4 | pub fn uniquify_id<'a>(ids: &mut HashSet>, id: Cow<'a, str>) -> Cow<'a, str> { 5 | if ids.contains(id.as_ref()) { 6 | let (prefix, mut count) = if let Some((prefix, counter)) = id.rsplit_once('_') { 7 | if counter.chars().all(|c| c.is_ascii_digit()) { 8 | let count = counter.parse::().unwrap_or_default() + 1; 9 | (prefix, count) 10 | } else { 11 | (id.as_ref(), 2) 12 | } 13 | } else { 14 | (id.as_ref(), 2) 15 | }; 16 | let mut new_id = format!("{prefix}_{count}"); 17 | while ids.contains(new_id.as_str()) && count < 666 { 18 | count += 1; 19 | new_id = format!("{prefix}_{count}"); 20 | } 21 | ids.insert(Cow::Owned(new_id.clone())); 22 | return Cow::Owned(new_id); 23 | } 24 | let id_ = id.clone().to_string(); 25 | ids.insert(Cow::Owned(id_)); 26 | id 27 | } 28 | -------------------------------------------------------------------------------- /crates/rari-md/src/error.rs: -------------------------------------------------------------------------------- 1 | use rari_types::ArgError; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Clone, Error)] 5 | pub enum DocError { 6 | #[error("pest error: {0}")] 7 | PestError(String), 8 | #[error("failed to decode ks: {0}")] 9 | DecodeError(#[from] base64::DecodeError), 10 | #[error("failed to convert ks: {0}")] 11 | Utf8Error(#[from] std::str::Utf8Error), 12 | #[error("failed to de/serialize")] 13 | SerializationError, 14 | } 15 | 16 | #[derive(Debug, Error)] 17 | pub enum RariFError { 18 | #[error(transparent)] 19 | DocError(#[from] DocError), 20 | #[error(transparent)] 21 | ArgError(#[from] ArgError), 22 | #[error("macro not implemented")] 23 | MacroNotImplemented, 24 | #[error("unknown macro")] 25 | UnknownMacro, 26 | } 27 | 28 | #[derive(Debug, Error)] 29 | pub enum MarkdownError { 30 | #[error("unable to output html for markdown")] 31 | HTMLFormatError, 32 | #[error(transparent)] 33 | IOError(#[from] std::io::Error), 34 | #[error(transparent)] 35 | DocError(#[from] DocError), 36 | } 37 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/glossarydisambiguation.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::helpers::subpages::get_sub_pages; 5 | use crate::helpers::summary_hack::{get_hacky_summary_md, strip_paragraph_unchecked}; 6 | use crate::pages::page::PageLike; 7 | 8 | #[rari_f(register = "crate::Templ")] 9 | pub fn glossarydisambiguation() -> Result { 10 | let mut out = String::new(); 11 | let pages = get_sub_pages( 12 | env.url, 13 | Some(1), 14 | crate::helpers::subpages::SubPagesSorter::Title, 15 | )?; 16 | out.push_str("
"); 17 | 18 | for page in pages { 19 | let summary = get_hacky_summary_md(&page)?; 20 | out.extend([ 21 | r#"
"#, 24 | &html_escape::encode_safe(page.title()), 25 | r#"
"#, 26 | strip_paragraph_unchecked(summary.as_str()), 27 | r#"
"#, 28 | ]); 29 | } 30 | out.push_str("
"); 31 | 32 | Ok(out) 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | vscode-ripgrep 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /rari-npm/LICENSE.MIT: -------------------------------------------------------------------------------- 1 | vscode-ripgrep 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /crates/rari-tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-tools" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors = [ 6 | "Andi Pieper ", 7 | "The MDN Engineering Team ", 8 | ] 9 | license.workspace = true 10 | rust-version.workspace = true 11 | 12 | [dependencies] 13 | rari-types.workspace = true 14 | rari-utils.workspace = true 15 | rari-doc.workspace = true 16 | thiserror.workspace = true 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | chrono.workspace = true 20 | tracing.workspace = true 21 | reqwest.workspace = true 22 | url.workspace = true 23 | indoc.workspace = true 24 | rayon.workspace = true 25 | sha2.workspace = true 26 | serde_yaml_ng.workspace = true 27 | pretty_yaml.workspace = true 28 | yaml_parser.workspace = true 29 | const_format.workspace = true 30 | dialoguer.workspace = true 31 | html-escape.workspace = true 32 | 33 | csv = "1" 34 | 35 | [dev-dependencies] 36 | serial_test = { version = "3", features = ["file_locks"] } 37 | rari-types = { workspace = true, features = ["testing"] } 38 | fake = { version = "4", features = ["chrono", "serde_json"] } 39 | rand = "0.9" 40 | assert-json-diff = "2" 41 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bump-minor-pre-major": true, 3 | "bump-patch-for-minor-pre-major": true, 4 | "changelog-sections": [ 5 | { "type": "feat", "section": "Features", "hidden": false }, 6 | { "type": "fix", "section": "Bug Fixes", "hidden": false }, 7 | { "type": "enhance", "section": "Enhancements", "hidden": false }, 8 | { "type": "chore", "section": "Miscellaneous", "hidden": false } 9 | ], 10 | "include-component-in-tag": false, 11 | "include-v-in-tag": true, 12 | "packages": { 13 | ".": { 14 | "component": "rari", 15 | "release-type": "rust", 16 | "include-component-in-tag": false, 17 | "include-v-in-tag": true, 18 | "extra-files": [ 19 | { 20 | "type": "json", 21 | "path": "rari-npm/package.json", 22 | "jsonpath": "$.version" 23 | }, 24 | { 25 | "type": "json", 26 | "path": "rari-npm/package-lock.json", 27 | "jsonpath": "$.version" 28 | }, 29 | { 30 | "type": "json", 31 | "path": "rari-npm/package-lock.json", 32 | "jsonpath": "$.packages[''].version" 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/rari-deps/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum DepsError { 5 | #[error(transparent)] 6 | JsonError(#[from] serde_json::Error), 7 | #[error(transparent)] 8 | IoError(#[from] std::io::Error), 9 | #[error(transparent)] 10 | RariIoError(#[from] rari_utils::error::RariIoError), 11 | #[error(transparent)] 12 | FetchError(#[from] reqwest::Error), 13 | #[error(transparent)] 14 | HeaderError(#[from] reqwest::header::ToStrError), 15 | #[error("no version for webref")] 16 | WebRefMissingVersionError, 17 | #[error("no tarball for webref")] 18 | WebRefMissingTarballError, 19 | #[error("Invalid github version")] 20 | InvalidGitHubVersion, 21 | #[error("Invalid github version")] 22 | VersionNotFound, 23 | #[error("Invalid url: {0}")] 24 | UrlError(#[from] url::ParseError), 25 | #[error("Version key not found in package.json")] 26 | PackageVersionNotFound, 27 | #[error("Version from package.json could not be parsed")] 28 | PackageVersionParseError, 29 | #[error("GitHub error: {0}")] 30 | GithubError(String), 31 | #[error("Webref error: {0}")] 32 | WebRefParseError(String), 33 | } 34 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/subpages_with_summaries.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::helpers::subpages::{SubPagesSorter, get_sub_pages}; 5 | use crate::helpers::summary_hack::{get_hacky_summary_md, strip_paragraph_unchecked}; 6 | use crate::pages::page::PageLike; 7 | 8 | #[rari_f(register = "crate::Templ")] 9 | pub fn subpageswithsummaries() -> Result { 10 | let mut out = String::new(); 11 | let sub_pages = get_sub_pages(env.url, Some(1), SubPagesSorter::Title)?; 12 | 13 | out.push_str("
"); 14 | for page in sub_pages { 15 | out.extend([ 16 | r#"
"#, 19 | &html_escape::encode_safe(page.title()), 20 | r#"

"#, 21 | strip_paragraph_unchecked(get_hacky_summary_md(&page)?.as_str()), 22 | r#"

"#, 23 | ]); 24 | } 25 | out.push_str("
"); 26 | Ok(out) 27 | } 28 | 29 | #[rari_f(register = "crate::Templ")] 30 | pub fn landingpagelistsubpages() -> Result { 31 | subpageswithsummaries(env) 32 | } 33 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/inline_labels.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_utils::concat_strs; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::l10n::l10n_json_data; 6 | 7 | #[rari_f(register = "crate::Templ")] 8 | pub fn securecontext_inline() -> Result { 9 | let label = l10n_json_data("Template", "secure_context_label", env.locale)?; 10 | let copy = l10n_json_data("Template", "secure_context_inline_copy", env.locale)?; 11 | 12 | Ok(write_inline_label(label, copy, "secure")) 13 | } 14 | 15 | #[rari_f(register = "crate::Templ")] 16 | pub fn readonlyinline() -> Result { 17 | let copy = l10n_json_data("Template", "readonly_badge_title", env.locale)?; 18 | let label = l10n_json_data("Template", "readonly_badge_abbreviation", env.locale)?; 19 | 20 | Ok(write_inline_label(label, copy, "readonly")) 21 | } 22 | 23 | pub fn write_inline_label(label: &str, copy: &str, typ: &str) -> String { 24 | concat_strs!( 25 | r#""#, 30 | label, 31 | "" 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-and-upload-binaries.yml: -------------------------------------------------------------------------------- 1 | name: build-and-upload-binaries 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | upload-assets: 12 | strategy: 13 | matrix: 14 | include: 15 | - target: x86_64-unknown-linux-musl 16 | os: ubuntu-latest 17 | - target: aarch64-unknown-linux-musl 18 | os: ubuntu-latest 19 | - target: aarch64-apple-darwin 20 | os: macos-latest 21 | - target: x86_64-apple-darwin 22 | os: macos-latest 23 | - target: x86_64-pc-windows-msvc 24 | os: windows-latest 25 | - target: aarch64-pc-windows-msvc 26 | os: windows-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 30 | with: 31 | persist-credentials: false 32 | - uses: taiki-e/upload-rust-binary-action@3962470d6e7f1993108411bc3f75a135ec67fc8c # v1.27.0 33 | with: 34 | bin: rari 35 | target: ${{ matrix.target }} 36 | build-tool: ${{ matrix.build-tool }} 37 | profile: release-lto 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm-manual.yml: -------------------------------------------------------------------------------- 1 | name: publish-npm-manual 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | publish: 7 | description: "actually publish" 8 | required: true 9 | default: false 10 | type: boolean 11 | 12 | # No GITHUB_TOKEN permissions, as we don't use it. 13 | permissions: {} 14 | 15 | jobs: 16 | test-run: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Setup 20 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Checkout 25 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 26 | with: 27 | registry-url: "https://registry.npmjs.org/" 28 | node-version-file: "./rari-npm/package.json" 29 | package-manager-cache: false 30 | 31 | - name: Test Publish 32 | if: ${{ ! inputs.publish }} 33 | working-directory: ./rari-npm 34 | run: npm publish --access public --dry-run 35 | 36 | - name: Publish 37 | if: ${{ inputs.publish }} 38 | working-directory: ./rari-npm 39 | run: npm publish --access public 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/parents.rs: -------------------------------------------------------------------------------- 1 | use super::title::transform_title; 2 | use crate::pages::json::Parent; 3 | use crate::pages::page::{Page, PageLike}; 4 | 5 | pub fn parents(doc: &T) -> Vec { 6 | let mut url = doc.url(); 7 | let mut parents = vec![Parent { 8 | uri: url.into(), 9 | title: doc 10 | .short_title() 11 | .unwrap_or(transform_title(doc.title())) 12 | .to_string(), 13 | }]; 14 | let doc_slug_no_slash = doc.base_slug().trim_end_matches('/'); 15 | while let Some(i) = url.trim_end_matches('/').rfind('/') { 16 | let parent_url = &url[..if doc.trailing_slash() { i + 1 } else { i }]; 17 | if parent_url 18 | .trim_end_matches('/') 19 | .ends_with(doc_slug_no_slash) 20 | { 21 | break; 22 | } 23 | if let Ok(parent) = Page::from_url_with_fallback(parent_url) { 24 | parents.push(Parent { 25 | uri: parent.url().into(), 26 | title: parent 27 | .short_title() 28 | .unwrap_or(transform_title(parent.title())) 29 | .to_string(), 30 | }) 31 | } 32 | url = parent_url 33 | } 34 | parents.reverse(); 35 | parents 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: "Issue report" 2 | description: Report an unexpected problem or unintended behavior. 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Before you start 9 | 10 | **Want to fix the problem yourself?** This project is open source and we welcome fixes and improvements from the community! 11 | 12 | ↩ Check the project [CONTRIBUTING.md](https://github.com/mdn/rari/blob/main/CONTRIBUTING.md) guide to see how to get started. 13 | 14 | --- 15 | - type: textarea 16 | id: problem 17 | attributes: 18 | label: What information was incorrect, unhelpful, or incomplete? 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: expected 23 | attributes: 24 | label: What did you expect to see? 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: references 29 | attributes: 30 | label: Do you have any supporting links, references, or citations? 31 | description: Link to information that helps us confirm your issue. 32 | - type: textarea 33 | id: more-info 34 | attributes: 35 | label: Do you have anything more you want to share? 36 | description: For example, steps to reproduce, screenshots, screen recordings, or sample code. 37 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/svgattr.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::templ::api::RariApi; 5 | 6 | /// Creates a link to an SVG attribute reference page on MDN. 7 | /// 8 | /// This macro generates links to SVG attribute documentation. It formats 9 | /// the link with the attribute name and applies code formatting to distinguish 10 | /// SVG attributes in the text. 11 | /// 12 | /// # Arguments 13 | /// * `name` - The SVG attribute name (e.g., "fill", "stroke", "viewBox") 14 | /// 15 | /// # Examples 16 | /// * `{{SVGAttr("fill")}}` -> links to fill attribute with code formatting 17 | /// * `{{SVGAttr("stroke-width")}}` -> links to stroke-width attribute 18 | /// * `{{SVGAttr("viewBox")}}` -> links to viewBox attribute 19 | /// 20 | /// # Special handling 21 | /// - Uses the attribute name as both the URL path and display text 22 | /// - Applies code formatting (`` tags) by default 23 | /// - Links to `/Web/SVG/Reference/Attribute/{name}` path structure 24 | #[rari_f(register = "crate::Templ")] 25 | pub fn svgattr(name: String) -> Result { 26 | let url = format!( 27 | "/{}/docs/Web/SVG/Reference/Attribute/{}", 28 | env.locale.as_url_str(), 29 | name, 30 | ); 31 | 32 | RariApi::link(&url, Some(env.locale), Some(&name), true, None, false) 33 | } 34 | -------------------------------------------------------------------------------- /crates/rari-tools/src/git.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::path::Path; 3 | use std::process::{Command, Output}; 4 | 5 | /// Run `git` with `args` with `root` as current directory. 6 | /// For tests this will run the first arg as command, eg.: 7 | /// Instead of `git mv foo bar` -> `mv foo bar`. 8 | pub fn exec_git_with_test_fallback(args: &[impl AsRef], root: impl AsRef) -> Output { 9 | let git = OsStr::new("git"); 10 | let echo = OsStr::new("echo"); 11 | 12 | let (command, args) = if cfg!(test) { 13 | ( 14 | args.first().map(AsRef::as_ref).unwrap_or(echo), 15 | &args[if args.is_empty() { 0 } else { 1 }..], 16 | ) 17 | } else { 18 | (git, args) 19 | }; 20 | exec_git_internal(command, args, root) 21 | } 22 | 23 | pub fn exec_git(args: &[impl AsRef], root: impl AsRef) -> Output { 24 | Command::new("git") 25 | .args(args) 26 | .current_dir(root) 27 | .output() 28 | .expect("failed to execute process") 29 | } 30 | 31 | fn exec_git_internal( 32 | command: impl AsRef, 33 | args: &[impl AsRef], 34 | root: impl AsRef, 35 | ) -> Output { 36 | Command::new(command) 37 | .args(args) 38 | .current_dir(root) 39 | .output() 40 | .expect("failed to execute process") 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm.yml: -------------------------------------------------------------------------------- 1 | name: publish-npm 2 | 3 | on: 4 | workflow_run: 5 | workflows: [build-and-upload-binaries] 6 | types: [completed] 7 | 8 | permissions: 9 | id-token: write # OIDC for npm Trusted Publishing 10 | 11 | jobs: 12 | on-success: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | steps: 16 | - name: Setup 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Checkout 22 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 23 | with: 24 | registry-url: "https://registry.npmjs.org/" 25 | node-version-file: "./rari-npm/package.json" 26 | package-manager-cache: false 27 | 28 | # Ensure npm 11.5.1 or later for trusted publishing 29 | - name: Install latest npm 30 | run: npm install -g npm@latest 31 | 32 | - name: Generate Schema 33 | working-directory: ./rari-npm 34 | run: npm install && npm run export-schema && npm run generate-types 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Publish 39 | working-directory: ./rari-npm 40 | run: npm publish --access public --provenance 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Overview 4 | 5 | This policy applies to MDN's website (`developer.mozilla.org`), backend services, and GitHub repositories in the [`mdn`](https://github.com/mdn) organization. Issues affecting other Mozilla products or services should be reported through the [Mozilla Security Bug Bounty Program](https://www.mozilla.org/en-US/security/bug-bounty/). 6 | 7 | For non-security issues, please file a [content bug](https://github.com/mdn/content/issues/new/choose), a [website bug](https://github.com/mdn/fred/issues/new/choose) or a [content/feature suggestion](https://github.com/mdn/mdn/issues/new/choose). 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover a potential security issue, please report it privately via . 12 | 13 | If you prefer not to use HackerOne, you can report it via . 14 | 15 | ## Bounty Program 16 | 17 | Vulnerabilities in MDN may qualify for Mozilla's Bug Bounty Program. Eligibility and reward amounts are described on . 18 | 19 | Please use the above channels even if you are not interested in a bounty reward. 20 | 21 | ## Responsible Disclosure 22 | 23 | Please do not publicly disclose details until Mozilla's security team and the MDN engineering team have verified and fixed the issue. 24 | 25 | We appreciate your efforts to keep MDN and its users safe. 26 | -------------------------------------------------------------------------------- /LICENSE.BSD-2-Clause: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017–2024, Asherah Connor and Comrak contributors 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/api_list_specs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use itertools::Itertools; 4 | use rari_templ_func::rari_f; 5 | 6 | use crate::error::DocError; 7 | use crate::helpers::json_data::json_data_group; 8 | use crate::helpers::subpages::write_li_with_badges; 9 | use crate::pages::types::doc::Doc; 10 | 11 | #[rari_f(register = "crate::Templ")] 12 | pub fn listgroups() -> Result { 13 | let group_data = json_data_group(); 14 | 15 | let mut out_by_letter = BTreeMap::new(); 16 | 17 | for (name, group) in group_data.iter().sorted_by(|(a, _), (b, _)| a.cmp(b)) { 18 | if let Some(overview) = group.overview.first() { 19 | let first_letter = name.chars().next().unwrap_or_default(); 20 | let page = Doc::page_from_slug( 21 | &format!("Web/API/{}", overview.replace(' ', "_")), 22 | env.locale, 23 | true, 24 | )?; 25 | let out = out_by_letter.entry(first_letter).or_default(); 26 | write_li_with_badges(out, &page, env.locale, false, true)?; 27 | } 28 | } 29 | 30 | let mut out = String::new(); 31 | out.push_str(r#"
"#); 32 | for (letter, content) in out_by_letter { 33 | out.push_str(r#"

"#); 34 | out.push(letter); 35 | out.extend([r#"

    "#, content.as_str(), r#"
"#]); 36 | } 37 | out.push_str(r#"
"#); 38 | 39 | Ok(out) 40 | } 41 | -------------------------------------------------------------------------------- /crates/rari-md/src/ctype.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017–2024, Asherah Connor and Comrak contributors 2 | // This code is part of Comrak and is licensed under the BSD 2-Clause License. 3 | // See LICENSE file for more information. 4 | // Modified by Florian Dieminger in 2024 5 | 6 | #[rustfmt::skip] 7 | const CMARK_CTYPE_CLASS: [u8; 256] = [ 8 | /* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ 9 | /* 0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 10 | /* 1 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11 | /* 2 */ 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 12 | /* 3 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 13 | /* 4 */ 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 14 | /* 5 */ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 2, 15 | /* 6 */ 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 16 | /* 7 */ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, 0, 17 | /* 8 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18 | /* 9 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19 | /* a */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20 | /* b */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21 | /* c */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | /* d */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23 | /* e */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | /* f */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25 | ]; 26 | 27 | pub fn isspace(ch: u8) -> bool { 28 | CMARK_CTYPE_CLASS[ch as usize] == 1 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/_test-content.yml: -------------------------------------------------------------------------------- 1 | name: Test Content 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | rari-binary-artifact-id: 7 | description: "ID of rari binary artifact (optional)" 8 | type: string 9 | default: "" 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | fix-flaws: 15 | uses: ./.github/workflows/_test-content-run.yml 16 | with: 17 | rari-binary-artifact-id: ${{ inputs.rari-binary-artifact-id }} 18 | command: content fix-flaws 19 | translated-content: true 20 | diff: true 21 | 22 | fmt-sidebars: 23 | uses: ./.github/workflows/_test-content-run.yml 24 | with: 25 | rari-binary-artifact-id: ${{ inputs.rari-binary-artifact-id }} 26 | command: content fmt-sidebars 27 | diff: true 28 | 29 | sync-sidebars: 30 | uses: ./.github/workflows/_test-content-run.yml 31 | with: 32 | rari-binary-artifact-id: ${{ inputs.rari-binary-artifact-id }} 33 | command: content sync-sidebars 34 | diff: true 35 | 36 | sync-translated-content: 37 | uses: ./.github/workflows/_test-content-run.yml 38 | with: 39 | rari-binary-artifact-id: ${{ inputs.rari-binary-artifact-id }} 40 | command: content sync-translated-content 41 | translated-content: true 42 | diff: true 43 | 44 | validate-redirects: 45 | uses: ./.github/workflows/_test-content-run.yml 46 | with: 47 | rari-binary-artifact-id: ${{ inputs.rari-binary-artifact-id }} 48 | command: content validate-redirects 49 | translated-content: true 50 | -------------------------------------------------------------------------------- /crates/rari-doc/src/html/banner.rs: -------------------------------------------------------------------------------- 1 | use rari_types::templ::TemplType; 2 | use rari_types::{Arg, Quotes}; 3 | use tracing::{Level, span}; 4 | 5 | use crate::error::DocError; 6 | use crate::pages::page::PageLike; 7 | use crate::pages::types::utils::FmTempl; 8 | use crate::templ::templs::invoke; 9 | 10 | pub fn build_banner(banner: &FmTempl, page: &T) -> Result { 11 | let rari_env = page.rari_env().ok_or(DocError::NoRariEnv)?; 12 | let (name, args) = match banner { 13 | FmTempl::NoArgs(name) => (name.as_str(), vec![]), 14 | FmTempl::WithArgs { name, args } => ( 15 | name.as_str(), 16 | args.iter() 17 | .map(|s| Some(Arg::String(s.clone(), Quotes::Double))) 18 | .collect(), 19 | ), 20 | }; 21 | let span = span!(Level::ERROR, "banner", banner = name,); 22 | let _enter = span.enter(); 23 | let rendered_banner = match invoke(&rari_env, name, args) { 24 | Ok((rendered_banner, TemplType::Banner)) => rendered_banner, 25 | Ok((_, typ)) => { 26 | let span = span!(Level::ERROR, "banner", banner = name,); 27 | let _enter = span.enter(); 28 | tracing::warn!("{typ} macro in banner frontmatter"); 29 | Default::default() 30 | } 31 | Err(e) => { 32 | let span = span!(Level::ERROR, "banner", banner = name,); 33 | let _enter = span.enter(); 34 | tracing::warn!("{e}"); 35 | Default::default() 36 | } 37 | }; 38 | Ok(rendered_banner) 39 | } 40 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/webext_all_examples.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::helpers::web_ext_examples::web_ext_examples_json; 5 | use crate::templ::api::RariApi; 6 | 7 | #[rari_f(register = "crate::Templ")] 8 | pub fn webextallexamples() -> Result { 9 | let mut out = String::new(); 10 | 11 | let all_examples = web_ext_examples_json(); 12 | 13 | out.extend([ 14 | r#""#, 15 | r#""#, 16 | ]); 17 | 18 | for example in all_examples { 19 | out.extend([ 20 | r#""#); 39 | } 40 | 41 | out.push_str(r#"
NameDescriptionJavaScript APIs
"#, 23 | &example.name, 24 | r#""#, 25 | &example.description, 26 | r#""#, 27 | ]); 28 | for api in &example.javascript_apis { 29 | let url = format!( 30 | "/{}/docs/Mozilla/Add-ons/WebExtensions/API/{}", 31 | env.locale.as_url_str(), 32 | &api.replace(' ', "_").replace("()", "").replace('.', "/"), 33 | ); 34 | let link = RariApi::link(&url, Some(env.locale), None, true, None, false)?; 35 | out.push_str(&link); 36 | out.push_str("
") 37 | } 38 | out.push_str(r#"
"#); 42 | 43 | Ok(out) 44 | } 45 | -------------------------------------------------------------------------------- /crates/rari-deps/src/external_json.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::{BufWriter, Write}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use chrono::{DateTime, Duration, Utc}; 6 | use rari_utils::io::read_to_string; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::client::get; 10 | use crate::error::DepsError; 11 | 12 | #[derive(Deserialize, Serialize, Default, Debug)] 13 | pub struct LastChecked { 14 | pub latest_last_check: Option>, 15 | } 16 | 17 | pub fn get_json(name: &str, url: &str, out_path: &Path) -> Result, DepsError> { 18 | let package_path = out_path.join(name); 19 | let last_check_path = package_path.join("last_check.json"); 20 | let now = Utc::now(); 21 | let current = read_to_string(last_check_path) 22 | .ok() 23 | .and_then(|current| serde_json::from_str::(¤t).ok()) 24 | .unwrap_or_default(); 25 | if current.latest_last_check.unwrap_or_default() < now - Duration::days(1) { 26 | if package_path.exists() { 27 | fs::remove_dir_all(&package_path)?; 28 | } 29 | fs::create_dir_all(&package_path)?; 30 | let buf = get(url)?.bytes()?; 31 | 32 | let out_file = package_path.join("data.json"); 33 | let file = File::create(out_file).unwrap(); 34 | let mut buffed = BufWriter::new(file); 35 | buffed.write_all(buf.as_ref())?; 36 | 37 | fs::write( 38 | package_path.join("last_check.json"), 39 | serde_json::to_string_pretty(&LastChecked { 40 | latest_last_check: Some(now), 41 | })?, 42 | )?; 43 | } 44 | Ok(None) 45 | } 46 | -------------------------------------------------------------------------------- /crates/css-syntax/src/syntax_provider.rs: -------------------------------------------------------------------------------- 1 | use css_syntax_types::{AtRule, Function, Property, SpecLink, Type}; 2 | 3 | // Define a trait for types that have syntax and spec_link 4 | pub trait SyntaxProvider { 5 | fn syntax(&self) -> &Option; 6 | fn spec_link(&self) -> &Option; 7 | fn extended_spec_links(&self) -> &Vec; 8 | } 9 | 10 | // Implement the trait for all the types we need 11 | impl SyntaxProvider for Property { 12 | fn syntax(&self) -> &Option { 13 | &self.syntax 14 | } 15 | fn spec_link(&self) -> &Option { 16 | &self.spec_link 17 | } 18 | fn extended_spec_links(&self) -> &Vec { 19 | &self.extended_spec_links 20 | } 21 | } 22 | 23 | impl SyntaxProvider for AtRule { 24 | fn syntax(&self) -> &Option { 25 | &self.syntax 26 | } 27 | fn spec_link(&self) -> &Option { 28 | &self.spec_link 29 | } 30 | fn extended_spec_links(&self) -> &Vec { 31 | &self.extended_spec_links 32 | } 33 | } 34 | 35 | impl SyntaxProvider for Function { 36 | fn syntax(&self) -> &Option { 37 | &self.syntax 38 | } 39 | fn spec_link(&self) -> &Option { 40 | &self.spec_link 41 | } 42 | fn extended_spec_links(&self) -> &Vec { 43 | &self.extended_spec_links 44 | } 45 | } 46 | 47 | impl SyntaxProvider for Type { 48 | fn syntax(&self) -> &Option { 49 | &self.syntax 50 | } 51 | fn spec_link(&self) -> &Option { 52 | &self.spec_link 53 | } 54 | fn extended_spec_links(&self) -> &Vec { 55 | &self.extended_spec_links 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/csp.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::templ::api::RariApi; 5 | 6 | /// Creates a link to a Content Security Policy (CSP) directive reference page on MDN. 7 | /// 8 | /// This macro generates links to CSP directive documentation under the 9 | /// Content-Security-Policy HTTP header reference. It formats the link with 10 | /// the directive name and applies code formatting to distinguish CSP directives 11 | /// in the text. 12 | /// 13 | /// # Arguments 14 | /// * `directive` - The CSP directive name (e.g., "default-src", "script-src", "img-src") 15 | /// 16 | /// # Examples 17 | /// * `{{CSP("default-src")}}` -> links to default-src directive with code formatting 18 | /// * `{{CSP("script-src")}}` -> links to script-src directive 19 | /// * `{{CSP("unsafe-inline")}}` -> links to unsafe-inline keyword 20 | /// * `{{CSP("nonce-")}}` -> links to nonce- source expression 21 | /// 22 | /// # Special handling 23 | /// - Uses the directive name as both the URL path and display text 24 | /// - Applies code formatting (`` tags) by default 25 | /// - Links to `/Web/HTTP/Reference/Headers/Content-Security-Policy/{directive}` path structure 26 | /// - Works for directives, keywords, and source expressions within CSP 27 | #[rari_f(register = "crate::Templ")] 28 | pub fn csp(directive: String) -> Result { 29 | let url = format!( 30 | "/{}/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/{directive}", 31 | env.locale.as_url_str() 32 | ); 33 | RariApi::link( 34 | &url, 35 | Some(env.locale), 36 | Some(directive.as_ref()), 37 | true, 38 | None, 39 | false, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/live_sample_link.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_utils::concat_strs; 3 | 4 | use crate::error::DocError; 5 | use crate::templ::api::RariApi; 6 | 7 | /// Creates a link to open a live sample in fullscreen mode. 8 | /// 9 | /// This macro generates a link that opens an embedded live code sample in a fullscreen 10 | /// view, providing users with a larger workspace to interact with and examine the example. 11 | /// The link targets the live sample by its heading ID and displays custom link text. 12 | /// 13 | /// # Arguments 14 | /// * `id` - ID of the heading that contains the live sample to link to 15 | /// * `display` - Display text for the link (e.g., "Open in fullscreen", "View full example") 16 | /// 17 | /// # Examples 18 | /// * `{{LiveSampleLink("Basic_example", "View full example")}}` -> creates fullscreen link for "Basic example" sample 19 | /// * `{{LiveSampleLink("Interactive_demo", "Open in fullscreen")}}` -> link with "Open in fullscreen" text 20 | /// * `{{LiveSampleLink("Complex_layout", "See full layout")}}` -> custom link text for complex examples 21 | /// 22 | /// # Special handling 23 | /// - Converts heading ID to anchor format for proper targeting 24 | /// - Uses fragment identifier with "livesample_fullscreen=" parameter 25 | /// - Creates accessible links with descriptive text 26 | /// - Works in conjunction with EmbedLiveSample macro for complete functionality 27 | #[rari_f(register = "crate::Templ")] 28 | pub fn livesamplelink(id: String, display: String) -> Result { 29 | let id = RariApi::anchorize(&id); 30 | Ok(concat_strs!( 31 | r##""#, 34 | &display, 35 | "" 36 | )) 37 | } 38 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/js_property_attributes.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | 3 | use crate::error::DocError; 4 | use crate::helpers::l10n::l10n_json_data; 5 | 6 | #[rari_f(register = "crate::Templ")] 7 | pub fn js_property_attributes( 8 | writable: i64, 9 | enumerable: i64, 10 | configurable: i64, 11 | ) -> Result { 12 | let writable = writable != 0; 13 | let enumerable = enumerable != 0; 14 | let configurable = configurable != 0; 15 | let yes = l10n_json_data("Template", "yes", env.locale)?; 16 | let no = l10n_json_data("Template", "no", env.locale)?; 17 | 18 | let mut out = String::new(); 19 | out.extend([ 20 | r#"
"#, 21 | l10n_json_data( 22 | "Template", 23 | "js_property_attributes_header_prefix", 24 | env.locale, 25 | )?, 26 | "", 27 | env.title, 28 | "", 29 | l10n_json_data( 30 | "Template", 31 | "js_property_attributes_header_suffix", 32 | env.locale, 33 | )?, 34 | r#"
"#, 35 | l10n_json_data("Template", "writable", env.locale)?, 36 | r#""#, 37 | if writable { yes } else { no }, 38 | r#"
"#, 39 | l10n_json_data("Template", "enumerable", env.locale)?, 40 | r#""#, 41 | if enumerable { yes } else { no }, 42 | r#"
"#, 43 | l10n_json_data("Template", "configurable", env.locale)?, 44 | r#""#, 45 | if configurable { yes } else { no }, 46 | r#"
"#, 47 | ]); 48 | Ok(out) 49 | } 50 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/api_list_alpha.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::fm_types::PageType; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::subpages::{SubPagesSorter, get_sub_pages}; 6 | use crate::pages::page::PageLike; 7 | use crate::templ::api::RariApi; 8 | 9 | #[rari_f(register = "crate::Templ")] 10 | pub fn apilistalpha() -> Result { 11 | let mut out = String::new(); 12 | let pages = get_sub_pages("/en-US/docs/Web/API", Some(1), SubPagesSorter::Title)?; 13 | 14 | let mut current_letter = None; 15 | 16 | out.push_str(r#"
"#); 17 | for page in pages 18 | .iter() 19 | .filter(|page| page.page_type() == PageType::WebApiInterface) 20 | { 21 | let first_letter = page.title().chars().next(); 22 | 23 | if first_letter != current_letter { 24 | if current_letter.is_some() { 25 | out.push_str(""); 26 | } 27 | current_letter = first_letter; 28 | if let Some(current_letter) = current_letter { 29 | out.push_str("

"); 30 | out.push_str(&html_escape::encode_safe( 31 | current_letter.encode_utf8(&mut [0; 4]), 32 | )); 33 | out.push_str("

    "); 34 | } 35 | } 36 | out.extend([ 37 | "
  • ", 38 | &RariApi::link( 39 | page.url(), 40 | Some(env.locale), 41 | None, 42 | true, 43 | Some(page.short_title().unwrap_or(page.title())), 44 | true, 45 | )?, 46 | "
  • ", 47 | ]); 48 | } 49 | out.push_str(r#"
"#); 50 | 51 | Ok(out) 52 | } 53 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/cssinfo.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use rari_templ_func::rari_f; 4 | use serde_json::Value; 5 | 6 | use crate::error::DocError; 7 | use crate::helpers::css_info::{ 8 | css_info_properties, mdn_data_files, write_computed_output, write_missing, 9 | }; 10 | 11 | #[rari_f(register = "crate::Templ")] 12 | pub fn cssinfo() -> Result { 13 | let name = env 14 | .slug 15 | .rsplit('/') 16 | .next() 17 | .map(str::to_lowercase) 18 | .unwrap_or_default(); 19 | 20 | let at_rule = env 21 | .slug 22 | .strip_prefix("Web/CSS/Reference/At-rules/") 23 | .and_then(|at_rule| { 24 | if at_rule.starts_with('@') { 25 | Some(&at_rule[..at_rule.find('/').unwrap_or(at_rule.len())]) 26 | } else { 27 | None 28 | } 29 | }); 30 | 31 | let data = mdn_data_files(); 32 | let css_info_data = if let Some(at_rule) = at_rule { 33 | &data.css_at_rules.get(at_rule).unwrap_or(&Value::Null)["descriptors"][&name] 34 | } else { 35 | data.css_properties.get(&name).unwrap_or(&Value::Null) 36 | }; 37 | let props = css_info_properties(at_rule, env.locale, css_info_data)?; 38 | 39 | let mut out = String::new(); 40 | 41 | if props.is_empty() { 42 | write_missing(&mut out, env.locale)?; 43 | return Ok(out); 44 | } 45 | out.push_str(r#""#); 46 | for (name, label) in props { 47 | write!(&mut out, r#""#)?; 50 | } 51 | out.push_str(r#"
{label}"#)?; 48 | write_computed_output(env, &mut out, env.locale, css_info_data, name, at_rule)?; 49 | write!(&mut out, r#"
"#); 52 | Ok(out) 53 | } 54 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/svgxref.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | use rari_types::locale::Locale; 4 | 5 | use crate::error::DocError; 6 | use crate::templ::api::RariApi; 7 | 8 | /// Creates a link to an SVG element reference page on MDN. 9 | /// 10 | /// This macro generates links to SVG element documentation. It automatically 11 | /// formats the display text with angle brackets (e.g., ``) and applies 12 | /// code formatting to distinguish SVG elements in the text. 13 | /// 14 | /// # Arguments 15 | /// * `element_name` - The SVG element name (without angle brackets) 16 | /// * `_` - Unused parameter (kept for compatibility) 17 | /// 18 | /// # Examples 19 | /// * `{{SVGElement("circle")}}` -> links to `` element with code formatting 20 | /// * `{{SVGElement("path")}}` -> links to `` element 21 | /// * `{{SVGElement("svg")}}` -> links to root `` element 22 | /// 23 | /// # Special handling 24 | /// - Automatically wraps element name in `<` and `>` for display 25 | /// - Uses code formatting (`` tags) by default 26 | /// - Links to `/Web/SVG/Reference/Element/{element_name}` path structure 27 | #[rari_f(register = "crate::Templ")] 28 | pub fn svgelement(element_name: String, _: Option) -> Result { 29 | svgxref_internal(&element_name, env.locale) 30 | } 31 | 32 | pub fn svgxref_internal(element_name: &str, locale: Locale) -> Result { 33 | let display = format!("<{element_name}>"); 34 | let url = format!( 35 | "/{}/docs/Web/SVG/Reference/Element/{}", 36 | locale.as_url_str(), 37 | element_name, 38 | ); 39 | 40 | RariApi::link( 41 | &url, 42 | Some(locale), 43 | Some(display.as_ref()), 44 | true, 45 | None, 46 | false, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | # See https://github.com/apps/settings for all available settings. 3 | 4 | # The name of the repository. Changing this will rename the repository 5 | name: project-template 6 | 7 | # A short description of the repository that will show up on GitHub 8 | description: MDN Web Docs project template 9 | 10 | # A URL with more information about the repository 11 | homepage: https://github.com/mdn/project-template 12 | 13 | # The branch used by default for pull requests and when the repository is cloned/viewed. 14 | default_branch: main 15 | 16 | # This repository is a template that others can use to start a new repository. 17 | is_template: true 18 | 19 | branches: 20 | - name: main 21 | protection: 22 | # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. 23 | required_pull_request_reviews: 24 | # The number of approvals required. (1-6) 25 | required_approving_review_count: 1 26 | # Dismiss approved reviews automatically when a new commit is pushed. 27 | dismiss_stale_reviews: true 28 | # Blocks merge until code owners have reviewed. 29 | require_code_owner_reviews: true 30 | 31 | collaborators: 32 | - username: Rumyra 33 | permission: admin 34 | 35 | - username: fiji-flo 36 | permission: admin 37 | 38 | labels: 39 | - name: bug 40 | color: D41130 41 | description: "Something that's wrong or not working as expected" 42 | - name: chore 43 | color: 258CD3 44 | description: "A routine task" 45 | - name: "good first issue" 46 | color: 48B71D 47 | description: "Great for newcomers to start contributing" 48 | - name: "help wanted" 49 | color: 2E7A10 50 | description: "Contributions welcome" 51 | 52 | teams: 53 | - name: core 54 | permission: admin 55 | -------------------------------------------------------------------------------- /crates/rari-doc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rari-doc" 3 | version = "0.2.8" 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | rari-utils.workspace = true 11 | rari-types.workspace = true 12 | rari-md.workspace = true 13 | rari-data.workspace = true 14 | rari-templ-func.workspace = true 15 | 16 | thiserror.workspace = true 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | regex.workspace = true 20 | tracing.workspace = true 21 | url.workspace = true 22 | itertools.workspace = true 23 | constcat.workspace = true 24 | indexmap.workspace = true 25 | sha2.workspace = true 26 | tracing-subscriber.workspace = true 27 | dashmap.workspace = true 28 | pretty_yaml.workspace = true 29 | serde_yaml_ng.workspace = true 30 | yaml_parser.workspace = true 31 | schemars.workspace = true 32 | strum.workspace = true 33 | inventory.workspace = true 34 | tree-sitter.workspace = true 35 | tree-sitter-mdn.workspace = true 36 | 37 | yaml-rust = "0.4" 38 | percent-encoding = "2" 39 | validator = { version = "0.20", features = ["derive"] } 40 | chrono = { version = "0.4", features = ["serde"] } 41 | scraper = { version = "0.25", features = ["deterministic"] } 42 | lol_html = "2" 43 | html-escape = "0.2" 44 | html5ever = "0.36" 45 | ego-tree = "0.10" 46 | rss = { version = "2", features = [], default-features = false } 47 | cssparser = "0.36" 48 | 49 | icu_locale_core = "2" 50 | ignore = "0.4" 51 | crossbeam-channel = "0.5" 52 | rayon = "1" 53 | enum_dispatch = "0.3" 54 | icu_collator = "2" 55 | imagesize = "0.14" 56 | svg_metadata = "0.5" 57 | memoize = "0.5" 58 | unescaper = "0.1" 59 | 60 | 61 | css-syntax = { path = "../css-syntax", features = ["rari"] } 62 | 63 | [dev-dependencies] 64 | rari-types = { path = "../rari-types", features = ["testing"] } 65 | indoc.workspace = true 66 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/listsubpages.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::subpages::{self, ListSubPagesContext, SubPagesSorter}; 6 | 7 | /// List sub pages 8 | #[rari_f(register = "crate::Templ")] 9 | pub fn listsubpages( 10 | url: Option, 11 | depth: Option, 12 | reverse: Option, 13 | ordered: Option, 14 | ) -> Result { 15 | let depth = depth.map(|d| d.as_int() as usize).unwrap_or(1); 16 | let url = url.as_deref().filter(|s| !s.is_empty()).unwrap_or(env.url); 17 | let ordered = ordered.as_ref().map(AnyArg::as_bool).unwrap_or_default(); 18 | let mut out = String::new(); 19 | out.push_str(if ordered { "
    " } else { "
      " }); 20 | if reverse.map(|r| r.as_int() != 0).unwrap_or_default() { 21 | // Yes the old marco checks for == 0 not === 0. 22 | if depth > 1 { 23 | return Err(DocError::InvalidTempl( 24 | "listsubpages with reverse set and depth != 1".to_string(), 25 | )); 26 | } 27 | subpages::list_sub_pages_reverse_internal( 28 | &mut out, 29 | url, 30 | env.locale, 31 | Some(SubPagesSorter::Title), 32 | &[], 33 | false, 34 | )?; 35 | } else { 36 | subpages::list_sub_pages_nested_internal( 37 | &mut out, 38 | url, 39 | env.locale, 40 | Some(depth), 41 | ListSubPagesContext { 42 | sorter: Some(SubPagesSorter::Title), 43 | page_types: &[], 44 | code: false, 45 | include_parent: false, 46 | }, 47 | )?; 48 | } 49 | out.push_str(if ordered { "
" } else { "" }); 50 | 51 | Ok(out) 52 | } 53 | -------------------------------------------------------------------------------- /crates/rari-md/src/p.rs: -------------------------------------------------------------------------------- 1 | use comrak::nodes::{AstNode, NodeValue}; 2 | 3 | use crate::ext::{DELIM_END, DELIM_END_LEN, DELIM_START, DELIM_START_LEN}; 4 | 5 | fn only_escaped_templ(b: &[u8]) -> bool { 6 | let b = b.trim_ascii_end().trim_ascii_start(); 7 | if b.starts_with(DELIM_START.as_bytes()) { 8 | let start = DELIM_START_LEN; 9 | if let Some(end) = b[start..] 10 | .windows(DELIM_END_LEN) 11 | .position(|window| window == DELIM_END.as_bytes()) 12 | { 13 | if start + end + DELIM_END_LEN == b.len() { 14 | return true; 15 | } else { 16 | return only_escaped_templ(&b[start + end + DELIM_END_LEN..]); 17 | } 18 | } 19 | } 20 | false 21 | } 22 | 23 | pub(crate) fn is_escaped_templ_p<'a>(p: &'a AstNode<'a>) -> bool { 24 | p.children().all(|child| match &child.data.borrow().value { 25 | NodeValue::Text(t) => only_escaped_templ(t.as_bytes()), 26 | NodeValue::SoftBreak => true, 27 | _ => false, 28 | }) 29 | } 30 | 31 | pub(crate) fn is_empty_p<'a>(p: &'a AstNode<'a>) -> bool { 32 | p.first_child().is_none() 33 | } 34 | 35 | pub(crate) fn fix_p<'a>(p: &'a AstNode<'a>) { 36 | for child in p.reverse_children() { 37 | p.insert_before(child) 38 | } 39 | p.detach(); 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use super::*; 45 | 46 | #[test] 47 | fn test_only_escaped_templ() { 48 | let b = "⟬0⟭".as_bytes(); 49 | assert!(only_escaped_templ(b)); 50 | let b = "⟬0⟭⟬1⟭".as_bytes(); 51 | assert!(only_escaped_templ(b)); 52 | let b = "⟬0⟭\n⟬1⟭".as_bytes(); 53 | assert!(only_escaped_templ(b)); 54 | let b = "⟬0⟭ ⟬1⟭".as_bytes(); 55 | assert!(only_escaped_templ(b)); 56 | let b = "⟬0⟭,⟬1⟭".as_bytes(); 57 | assert!(!only_escaped_templ(b)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/summary_hack.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use rari_md::{M2HOptions, m2h_internal}; 3 | use scraper::Html; 4 | 5 | use crate::error::DocError; 6 | use crate::pages::page::{Page, PageLike}; 7 | use crate::templ::render::render_for_summary; 8 | 9 | /// There's a few places were we still transplant content. 10 | /// Yari had a hidden hacky way to do this and we have to mimic this for now. 11 | pub fn get_hacky_summary_md(page: &Page) -> Result { 12 | let summary_md = page 13 | .content() 14 | .lines() 15 | .skip_while(|line| { 16 | line.trim().is_empty() 17 | || line.starts_with("{{") && line.ends_with("}}") 18 | || line.starts_with("##") 19 | }) 20 | .take_while(|line| { 21 | !(line.trim().is_empty() 22 | || (line.starts_with("{{") && line.ends_with("}}") || line.starts_with("##"))) 23 | }) 24 | .join("\n"); 25 | if summary_md.is_empty() { 26 | Ok(String::from("No summary found.")) 27 | } else { 28 | render_for_summary(&summary_md).and_then(|md| { 29 | Ok(m2h_internal( 30 | md.trim(), 31 | page.locale(), 32 | M2HOptions { sourcepos: false }, 33 | )?) 34 | }) 35 | } 36 | } 37 | 38 | /// Trims a `

` tag in good faith. 39 | /// This does not check if theres a `

` as root and will 40 | /// result in invalid html for input like: 41 | /// ```html 42 | ///

foo

bar 43 | /// ``` 44 | pub fn strip_paragraph_unchecked(input: &str) -> &str { 45 | let out = input.trim().strip_prefix("

").unwrap_or(input); 46 | 47 | (out.trim().strip_suffix("

").unwrap_or(out)) as _ 48 | } 49 | 50 | pub fn text_content(html_str: &str) -> String { 51 | let fragment = Html::parse_fragment(html_str); 52 | fragment.root_element().text().collect() 53 | } 54 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/web_ext_examples.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::locale::Locale; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::l10n::l10n_json_data; 6 | use crate::helpers::web_ext_examples::{WEB_EXT_EXAMPLES_DATA, WebExtExample}; 7 | 8 | #[rari_f(register = "crate::Templ")] 9 | pub fn webextexamples(heading: Option) -> Result { 10 | let mut split = env.slug.rsplitn(3, '/'); 11 | let leaf = split.next(); 12 | let parent = split.next(); 13 | let examples = match (parent, leaf) { 14 | (Some("API"), Some(leaf)) => WEB_EXT_EXAMPLES_DATA.by_module.get(leaf), 15 | (Some(parent), Some(leaf)) => WEB_EXT_EXAMPLES_DATA 16 | .by_module_and_api 17 | .get([parent, leaf].join(".").as_str()), 18 | _ => None, 19 | }; 20 | 21 | if let Some(examples) = examples { 22 | example_links(examples, heading.as_deref().unwrap_or("h3"), env.locale) 23 | } else { 24 | Ok(Default::default()) 25 | } 26 | } 27 | 28 | fn example_links( 29 | examples: &[&WebExtExample], 30 | heading: &str, 31 | locale: Locale, 32 | ) -> Result { 33 | let mut out = String::new(); 34 | if !examples.is_empty() { 35 | out.extend([ 36 | "<", 37 | heading, 38 | ">", 39 | l10n_json_data("Template", "example_extensions_heading", locale)?, 40 | ""); 54 | } 55 | Ok(out) 56 | } 57 | -------------------------------------------------------------------------------- /crates/rari-templ-func/tests/basic.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::{Arg, ArgError, Quotes, RariEnv}; 3 | 4 | #[test] 5 | fn basic() { 6 | #[rari_f] 7 | fn something(a: String) -> Result { 8 | Ok(format!("some {a}")) 9 | } 10 | 11 | #[rari_f] 12 | fn something_else(a: Option) -> Result { 13 | Ok(format!("else {}", a.unwrap_or_default())) 14 | } 15 | 16 | #[rari_f] 17 | fn many(a: i64, b: Option) -> Result { 18 | Ok(format!("{} {}", a, b.unwrap_or_default())) 19 | } 20 | 21 | #[rari_f] 22 | fn booly(b: Option) -> Result { 23 | Ok(format!("{}", b.unwrap_or_default())) 24 | } 25 | 26 | assert_eq!( 27 | something(&Default::default(), "foo".into()).unwrap(), 28 | "some foo" 29 | ); 30 | assert_eq!( 31 | something_any( 32 | &Default::default(), 33 | vec![Some(Arg::String("foo".into(), Quotes::Double))] 34 | ) 35 | .unwrap(), 36 | "some foo" 37 | ); 38 | assert_eq!( 39 | many_any( 40 | &Default::default(), 41 | vec![Some(Arg::Int(1)), Some(Arg::Int(2))] 42 | ) 43 | .unwrap(), 44 | "1 2" 45 | ); 46 | assert_eq!( 47 | many_any(&Default::default(), vec![Some(Arg::Int(1))]).unwrap(), 48 | "1 0" 49 | ); 50 | 51 | assert_eq!(booly_any(&Default::default(), vec![]).unwrap(), "false"); 52 | } 53 | 54 | #[test] 55 | fn env() { 56 | #[rari_f] 57 | fn something(a: String) -> Result { 58 | Ok(format!("some {}{}", env.title, a)) 59 | } 60 | assert_eq!( 61 | something( 62 | &RariEnv { 63 | title: "foo", 64 | ..Default::default() 65 | }, 66 | "bar".into() 67 | ) 68 | .unwrap(), 69 | "some foobar" 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /crates/rari-deps/src/bcd.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | use rari_types::globals::deps; 6 | use rari_utils::io::read_to_string; 7 | use serde_json::Value; 8 | 9 | use crate::error::DepsError; 10 | use crate::npm::get_package; 11 | 12 | pub fn update_bcd(base_path: &Path) -> Result<(), DepsError> { 13 | if let Some(path) = get_package("@mdn/browser-compat-data", &deps().bcd, base_path)? { 14 | extract_spec_urls(&path)?; 15 | } 16 | get_package("web-specs", &deps().web_specs, base_path)?; 17 | Ok(()) 18 | } 19 | 20 | pub fn gather_spec_urls(value: &Value, path: &str, map: &mut HashMap>) { 21 | match &value["__compat"]["spec_url"] { 22 | Value::String(spec_url) => { 23 | map.insert(path.to_string(), vec![spec_url.clone()]); 24 | } 25 | Value::Array(spec_urls) => { 26 | map.insert( 27 | path.to_string(), 28 | spec_urls 29 | .iter() 30 | .filter_map(|s| s.as_str().map(String::from)) 31 | .collect(), 32 | ); 33 | } 34 | _ => {} 35 | }; 36 | if let Value::Object(o) = value { 37 | for (k, v) in o.iter().filter(|(k, _)| *k != "__compat") { 38 | gather_spec_urls( 39 | v, 40 | &format!("{path}{}{k}", if path.is_empty() { "" } else { "." }), 41 | map, 42 | ) 43 | } 44 | } 45 | } 46 | 47 | pub fn extract_spec_urls(package_path: &Path) -> Result<(), DepsError> { 48 | let text = read_to_string(package_path.join("package/data.json"))?; 49 | let json: Value = serde_json::from_str(&text)?; 50 | let mut map: HashMap> = HashMap::new(); 51 | gather_spec_urls(&json, "", &mut map); 52 | let spec_urls_out_path = package_path.join("spec_urls.json"); 53 | fs::write(spec_urls_out_path, serde_json::to_string(&map)?)?; 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /rari-npm/lib/postinstall.js: -------------------------------------------------------------------------------- 1 | import { mkdir } from "fs/promises"; 2 | import { arch, platform } from "os"; 3 | import { join } from "path"; 4 | 5 | import { download, exists } from "./download.js"; 6 | import packageJson from "../package.json" with { type: "json" }; 7 | 8 | const VERSION = `v${packageJson.version}`; 9 | const BIN_PATH = join(import.meta.dirname, "../bin"); 10 | const FORCE = JSON.parse(process.env.FORCE || "false"); 11 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN; 12 | 13 | const TARGET_LOOKUP = { 14 | arm64: { 15 | darwin: "aarch64-apple-darwin", 16 | linux: "aarch64-unknown-linux-musl", 17 | win32: "aarch64-pc-windows-msvc", 18 | }, 19 | x64: { 20 | darwin: "x86_64-apple-darwin", 21 | linux: "x86_64-unknown-linux-musl", 22 | win32: "x86_64-pc-windows-msvc", 23 | }, 24 | }; 25 | 26 | async function getTarget() { 27 | const architecture = process.env.npm_config_arch || arch(); 28 | 29 | const target = TARGET_LOOKUP[architecture]?.[platform()]; 30 | if (!target) { 31 | throw new Error("Unknown platform: " + platform()); 32 | } 33 | return target; 34 | } 35 | 36 | /** 37 | * This function is adapted from vscode-ripgrep (https://github.com/microsoft/vscode-ripgrep) 38 | * Copyright (c) Microsoft, licensed under the MIT License 39 | * 40 | */ 41 | async function main() { 42 | const binPathExists = await exists(BIN_PATH); 43 | if (!FORCE && binPathExists) { 44 | console.log( 45 | `${BIN_PATH} already exists, exiting use FORCE=true to force install`, 46 | ); 47 | process.exit(0); 48 | } 49 | 50 | if (!binPathExists) { 51 | await mkdir(BIN_PATH); 52 | } 53 | 54 | const target = await getTarget(); 55 | const options = { 56 | version: VERSION, 57 | token: GITHUB_TOKEN, 58 | target, 59 | destDir: BIN_PATH, 60 | force: FORCE, 61 | }; 62 | try { 63 | await download(options); 64 | } catch (err) { 65 | console.error(`Downloading rari failed: ${err.stack}`); 66 | process.exit(1); 67 | } 68 | } 69 | 70 | await main(); 71 | -------------------------------------------------------------------------------- /crates/rari-doc/src/baseline.rs: -------------------------------------------------------------------------------- 1 | //! # Baseline Module 2 | //! 3 | //! The `baseline` module provides functionality for managing and accessing baseline support status 4 | //! for web features. It includes utilities for loading baseline data from files and retrieving 5 | //! support status for specific browser compatibility keys. 6 | use std::sync::LazyLock; 7 | 8 | use rari_data::baseline::{Baseline, WebFeatures}; 9 | use rari_types::globals::data_dir; 10 | use tracing::warn; 11 | 12 | static WEB_FEATURES: LazyLock> = LazyLock::new(|| { 13 | let web_features = 14 | WebFeatures::from_file(&data_dir().join("baseline").join("data.extended.json")); 15 | match web_features { 16 | Ok(web_features) => Some(web_features), 17 | Err(e) => { 18 | warn!("{e:?}"); 19 | None 20 | } 21 | } 22 | }); 23 | 24 | /// Retrieves the baseline support status for a given browser compatibility key. 25 | /// 26 | /// This function looks up the baseline support status for the provided browser compatibility key 27 | /// in the `WEB_FEATURES` static variable. If it contains the specified key, it returns the corresponding 28 | /// `SupportStatusWithByKey`. If the key is not found, it returns `None`. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `browser_compat` - A slice of strings that holds the browser compatibility keys to be looked up. This function 33 | /// only deals with single keys, so the slice should contain only one element. 34 | /// 35 | /// # Returns 36 | /// 37 | /// * `Option<&'static SupportStatusWithByKey>` - Returns `Some(&SupportStatusWithByKey)` if the key is found, 38 | /// or `None` if the key is not found or `WEB_FEATURES` is not initialized. 39 | pub(crate) fn get_baseline<'a>(browser_compat: &[String]) -> Option> { 40 | if let Some(ref web_features) = *WEB_FEATURES { 41 | return match &browser_compat { 42 | &[bcd_key] => web_features.baseline_by_bcd_key(bcd_key.as_str()), 43 | _ => None, 44 | }; 45 | } 46 | None 47 | } 48 | -------------------------------------------------------------------------------- /crates/rari-tools/src/tests/fixtures/sidebars.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use rari_types::globals::content_root; 4 | 5 | pub(crate) struct SidebarFixtures { 6 | do_not_remove: bool, 7 | } 8 | 9 | impl Default for SidebarFixtures { 10 | fn default() -> Self { 11 | Self::new_internal(vec!["sidebar:\nl10n:"], false) 12 | } 13 | } 14 | 15 | impl SidebarFixtures { 16 | pub fn new(data: Vec<&str>) -> Self { 17 | Self::new_internal(data, false) 18 | } 19 | 20 | #[allow(dead_code)] 21 | pub fn debug_new(data: Vec<&str>) -> Self { 22 | Self::new_internal(data, true) 23 | } 24 | 25 | fn new_internal(data: Vec<&str>, do_not_remove: bool) -> Self { 26 | let mut path = content_root().to_path_buf(); 27 | path.push("sidebars"); 28 | 29 | if !path.exists() { 30 | fs::create_dir(&path).unwrap(); 31 | } 32 | for (ct, d) in data.into_iter().enumerate() { 33 | let name = format!("sidebar_{ct}.yaml"); 34 | fs::write(path.join(name), d.as_bytes()).unwrap(); 35 | } 36 | 37 | SidebarFixtures { do_not_remove } 38 | } 39 | } 40 | 41 | impl Drop for SidebarFixtures { 42 | fn drop(&mut self) { 43 | if self.do_not_remove { 44 | tracing::info!("Leaving doc fixtures in place for debugging"); 45 | return; 46 | } 47 | // Perform cleanup actions, recursively remove all files 48 | // in the sidebars folder, and the sidebars folder as well 49 | let mut path = content_root().to_path_buf(); 50 | path.push("sidebars"); 51 | 52 | let entries = fs::read_dir(&path).unwrap(); 53 | 54 | for entry in entries { 55 | let entry = entry.unwrap(); 56 | let path = entry.path(); 57 | 58 | if path.is_dir() { 59 | fs::remove_dir_all(&path).unwrap(); 60 | } else { 61 | fs::remove_file(&path).unwrap(); 62 | } 63 | } 64 | fs::remove_dir_all(&path).unwrap(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/json_data.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::OnceLock; 3 | 4 | use rari_types::globals::content_root; 5 | use rari_utils::io::read_to_string; 6 | use serde::Deserialize; 7 | 8 | #[derive(Debug, Default, Clone, Deserialize)] 9 | pub struct InterfaceData { 10 | pub inh: String, 11 | } 12 | 13 | pub static JSON_DATA_INTERFACE: OnceLock> = OnceLock::new(); 14 | 15 | pub fn json_data_interface() -> &'static HashMap { 16 | JSON_DATA_INTERFACE.get_or_init(|| { 17 | let f = content_root().join("jsondata/InterfaceData.json"); 18 | let json_str = read_to_string(f).expect("unable to read interface data json"); 19 | let mut interface_data: Vec> = 20 | serde_json::from_str(&json_str).expect("unable to parse interface data json"); 21 | interface_data.pop().unwrap_or_default() 22 | }) 23 | } 24 | 25 | #[derive(Debug, Default, Clone, Deserialize)] 26 | pub struct GroupData { 27 | #[serde(default)] 28 | pub overview: Vec, 29 | #[serde(default)] 30 | pub guides: Vec, 31 | #[serde(default)] 32 | pub interfaces: Vec, 33 | #[serde(default)] 34 | pub methods: Vec, 35 | #[serde(default)] 36 | pub properties: Vec, 37 | #[serde(default)] 38 | pub events: Vec, 39 | #[serde(default)] 40 | pub tutorial: Vec, 41 | } 42 | 43 | pub static JSON_DATA_GROUP: OnceLock> = OnceLock::new(); 44 | 45 | pub fn json_data_group() -> &'static HashMap { 46 | JSON_DATA_GROUP.get_or_init(|| { 47 | let f = content_root().join("jsondata/GroupData.json"); 48 | let json_str = read_to_string(f).expect("unable to read interface data json"); 49 | let mut interface_data: Vec> = 50 | serde_json::from_str(&json_str).expect("unable to parse group data json"); 51 | interface_data.pop().unwrap_or_default() 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/mathmlxref.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_utils::concat_strs; 3 | 4 | use crate::error::DocError; 5 | use crate::templ::api::RariApi; 6 | 7 | /// Creates a link to a MathML element reference page on MDN. 8 | /// 9 | /// This macro generates links to MathML element documentation. It automatically 10 | /// formats the display text with angle brackets (e.g., ``) and applies 11 | /// code formatting to distinguish MathML elements in the text. The element name 12 | /// is converted to lowercase for consistency. 13 | /// 14 | /// # Arguments 15 | /// * `element_name` - The MathML element name (will be converted to lowercase) 16 | /// 17 | /// # Examples 18 | /// * `{{MathMLElement("math")}}` -> links to `` element with code formatting 19 | /// * `{{MathMLElement("mrow")}}` -> links to `` element 20 | /// * `{{MathMLElement("mi")}}` -> links to `` element for identifiers 21 | /// * `{{MathMLElement("mo")}}` -> links to `` element for operators 22 | /// 23 | /// # Special handling 24 | /// - Converts element name to lowercase for URL consistency 25 | /// - Automatically wraps element name in `<` and `>` for display 26 | /// - Sets title attribute with unescaped angle brackets for accessibility 27 | /// - Uses code formatting (`` tags) by default 28 | /// - Links to `/Web/MathML/Reference/Element/{element_name}` path structure 29 | #[rari_f(register = "crate::Templ")] 30 | pub fn mathmlelement(element_name: String) -> Result { 31 | let element_name = element_name.to_lowercase(); 32 | let display = concat_strs!("<", element_name.as_str(), ">"); 33 | let title = concat_strs!("<", element_name.as_str(), ">"); 34 | let url = concat_strs!( 35 | "/", 36 | env.locale.as_url_str(), 37 | "/docs/Web/MathML/Reference/Element/", 38 | element_name.as_str() 39 | ); 40 | 41 | RariApi::link( 42 | &url, 43 | Some(env.locale), 44 | Some(&display), 45 | true, 46 | Some(&title), 47 | false, 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /crates/rari-doc/src/pages/templates.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Copy, Serialize, Default, Deserialize)] 5 | pub enum SpaBuildTemplate { 6 | #[default] 7 | SpaUnknown, 8 | SpaNotFound, 9 | SpaHomepage, 10 | SpaObservatoryLanding, 11 | SpaObservatoryAnalyze, 12 | SpaAdvertise, 13 | SpaPlusLanding, 14 | SpaPlusCollections, 15 | SpaPlusCollectionsFrequentlyViewed, 16 | SpaPlusUpdates, 17 | SpaPlusSettings, 18 | SpaPlusAiHelp, 19 | SpaPlay, 20 | SpaSearch, 21 | } 22 | 23 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 24 | pub enum DocPageRenderer { 25 | #[default] 26 | Doc, 27 | } 28 | 29 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 30 | pub enum BlogRenderer { 31 | #[default] 32 | BlogPost, 33 | BlogIndex, 34 | } 35 | 36 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 37 | pub enum ContributorSpotlightRenderer { 38 | #[default] 39 | ContributorSpotlight, 40 | } 41 | 42 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 43 | pub enum GenericRenderer { 44 | #[default] 45 | GenericDoc, 46 | GenericAbout, 47 | GenericCommunity, 48 | } 49 | 50 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 51 | pub enum SpaRenderer { 52 | #[default] 53 | SpaUnknown, 54 | SpaNotFound, 55 | SpaObservatoryLanding, 56 | SpaObservatoryAnalyze, 57 | SpaAdvertise, 58 | SpaPlusLanding, 59 | SpaPlusCollections, 60 | SpaPlusCollectionsFrequentlyViewed, 61 | SpaPlusUpdates, 62 | SpaPlusSettings, 63 | SpaPlusAiHelp, 64 | SpaPlay, 65 | SpaSearch, 66 | } 67 | 68 | #[derive(Debug, Clone, Default, Serialize, JsonSchema)] 69 | pub enum HomeRenderer { 70 | #[default] 71 | Homepage, 72 | } 73 | 74 | #[derive(Debug, Clone, Serialize, Default, JsonSchema)] 75 | pub enum CurriculumRenderer { 76 | #[default] 77 | CurriculumDefault, 78 | CurriculumModule, 79 | CurriculumOverview, 80 | CurriculumLanding, 81 | CurriculumAbout, 82 | } 83 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/embed_youtube.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_utils::concat_strs; 5 | 6 | use crate::error::DocError; 7 | 8 | /// Embeds a YouTube video in an iframe with privacy-enhanced mode. 9 | /// 10 | /// This macro creates a responsive YouTube iframe embed using the privacy-enhanced 11 | /// youtube-nocookie.com domain, which provides better privacy protection by not 12 | /// setting tracking cookies until the user interacts with the video. 13 | /// 14 | /// # Arguments 15 | /// * `video_id` - YouTube video ID (the part after "v=" in YouTube URLs) 16 | /// * `title` - Optional descriptive title for accessibility (defaults to "YouTube video") 17 | /// 18 | /// # Examples 19 | /// * `{{EmbedYouTube("dQw4w9WgXcQ")}}` -> embeds video with default title 20 | /// * `{{EmbedYouTube("dQw4w9WgXcQ", "Rick Astley - Never Gonna Give You Up")}}` -> with custom title 21 | /// 22 | /// # Special handling 23 | /// - Uses youtube-nocookie.com for enhanced privacy protection 24 | /// - Sets standard video dimensions (560x315) for optimal display 25 | /// - Enables modern YouTube features via allow attribute: 26 | /// - accelerometer, autoplay, clipboard-write, encrypted-media, gyroscope, picture-in-picture 27 | /// - Includes allowfullscreen attribute for fullscreen video playback 28 | /// - HTML-escapes the title attribute for security and accessibility 29 | #[rari_f(register = "crate::Templ")] 30 | pub fn embedyoutube(video_id: String, title: Option) -> Result { 31 | let title = title 32 | .as_deref() 33 | .map(|s| html_escape::encode_double_quoted_attribute(s)) 34 | .unwrap_or(Cow::Borrowed("YouTube video")); 35 | Ok(concat_strs!( 36 | r#""# 42 | )) 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to `rari` 2 | 3 | > [!Warning] 4 | > This project is work in progress and lacking most of its documentation. 5 | > Anything might change and code will move a lot. We do not encourage using it yet. 6 | > We'll have an official announcement before we migrate, so stay tuned. 7 | 8 | `rari` is the build system for [MDN](https://developer.mozilla.org). 9 | 10 | `rari` is hosted by [MDN](https://github.com/mdn). 11 | 12 | ## Getting Started 13 | 14 | To get up and running, follow these steps: 15 | 16 | Make sure you have [Rust](https://www.rust-lang.org/) installed, otherwise go to [https://rustup.rs/](https://rustup.rs/). 17 | 18 | Clone this repository and run: 19 | 20 | ```plain 21 | cargo run -- --help 22 | ``` 23 | 24 | ### Configuration 25 | 26 | Create a `.config.toml` in the current working directory. 27 | Add the following: 28 | 29 | ```toml 30 | content_root = "//files" 31 | build_out_root = "/tmp/rari" 32 | ``` 33 | 34 | ## Contributing 35 | 36 | For now we're aiming for a parity rewrite of [yari's](https://github.com/mdn/yari) `yarn build -n`. Which generates the `index.json` 37 | for all docs. Until we reach that point the codebase will be unstable and may change at any point. Therefore we won't accept contributions for now. 38 | 39 | 44 | 45 | By participating in and contributing to our projects and discussions, you acknowledge that you have read and agree to our [Code of Conduct](CODE_OF_CONDUCT.md). 46 | 47 | ## Resources 48 | 49 | For more information about `rari`, see the following resources: 50 | 51 | To be updated... 52 | 53 | 54 | 55 | ## Communications 56 | 57 | If you have any questions, please reach out to us on [Discord](https://developer.mozilla.org/discord) 58 | 59 | ## License 60 | 61 | This project is licensed under the [Mozilla Public License 2.0](LICENSE.md). 62 | -------------------------------------------------------------------------------- /crates/css-definition-syntax/src/walk.rs: -------------------------------------------------------------------------------- 1 | use crate::error::SyntaxDefinitionError; 2 | use crate::parser::Node; 3 | 4 | pub struct WalkOptions { 5 | pub enter: fn(&Node, &mut T) -> Result<(), SyntaxDefinitionError>, 6 | pub leave: fn(&Node, &mut T) -> Result<(), SyntaxDefinitionError>, 7 | } 8 | 9 | fn noop(_: &Node, _: &mut T) -> Result<(), SyntaxDefinitionError> { 10 | Ok(()) 11 | } 12 | 13 | impl Default for WalkOptions { 14 | fn default() -> Self { 15 | Self { 16 | enter: noop, 17 | leave: noop, 18 | } 19 | } 20 | } 21 | 22 | pub fn walk( 23 | node: &Node, 24 | options: &WalkOptions, 25 | context: &mut T, 26 | ) -> Result<(), SyntaxDefinitionError> { 27 | (options.enter)(node, context)?; 28 | match node { 29 | Node::Group(group) => { 30 | for term in &group.terms { 31 | walk(term, options, context)?; 32 | } 33 | } 34 | Node::Multiplier(multiplier) => { 35 | walk(&multiplier.term, options, context)?; 36 | } 37 | Node::BooleanExpr(boolean_exp) => { 38 | walk(&boolean_exp.term, options, context)?; 39 | } 40 | Node::Token(_) 41 | | Node::Property(_) 42 | | Node::Type(_) 43 | | Node::Function(_) 44 | | Node::Keyword(_) 45 | | Node::Comma 46 | | Node::String(_) 47 | | Node::AtKeyword(_) => {} 48 | _ => Err(SyntaxDefinitionError::UnknownNodeType(node.clone()))?, 49 | } 50 | (options.leave)(node, context)?; 51 | Ok(()) 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | use super::*; 57 | use crate::parser::parse; 58 | 59 | #[test] 60 | fn test_walk() -> Result<(), SyntaxDefinitionError> { 61 | let syntax = parse(" | {0,0} ")?; 62 | 63 | walk( 64 | &syntax, 65 | &WalkOptions { 66 | enter: |_, _| Ok(()), 67 | leave: |_, _| Ok(()), 68 | }, 69 | &mut (), 70 | )?; 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/embed_gh_live_sample.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_types::AnyArg; 5 | 6 | use crate::error::DocError; 7 | 8 | /// Embeds a live code sample from the MDN GitHub repository. 9 | /// 10 | /// This macro creates an iframe that displays live code examples hosted on 11 | /// mdn.github.io. These are typically complete, runnable examples that demonstrate 12 | /// web technologies and APIs in action, hosted directly from the MDN organization's 13 | /// GitHub repositories. 14 | /// 15 | /// # Arguments 16 | /// * `path` - Path to the example on mdn.github.io (e.g., "dom-examples/web-audio-api/basic/") 17 | /// * `width` - Optional width for the iframe (in pixels or CSS units) 18 | /// * `height` - Optional height for the iframe (in pixels or CSS units) 19 | /// 20 | /// # Examples 21 | /// * `{{EmbedGHLiveSample("dom-examples/web-audio-api/basic/")}}` -> embeds Web Audio API example 22 | /// * `{{EmbedGHLiveSample("css-examples/flexbox/", "100%", "400")}}` -> with custom dimensions 23 | /// * `{{EmbedGHLiveSample("webextensions-examples/menu-demo/", "800", "600")}}` -> WebExtension example 24 | /// 25 | /// # Special handling 26 | /// - Links directly to https://mdn.github.io/{path} for live examples 27 | /// - Uses standard iframe embedding without additional security restrictions 28 | /// - Allows custom sizing for different types of examples 29 | /// - Provides direct access to fully functional web applications and demos 30 | #[rari_f(register = "crate::Templ")] 31 | pub fn embedghlivesample( 32 | path: String, 33 | width: Option, 34 | height: Option, 35 | ) -> Result { 36 | let mut out = String::new(); 37 | out.push_str(""#, 49 | ]); 50 | Ok(out) 51 | } 52 | -------------------------------------------------------------------------------- /crates/css-definition-syntax/src/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::SyntaxDefinitionError; 2 | 3 | pub struct Tokenizer { 4 | pub str: Vec, 5 | pub pos: usize, 6 | } 7 | 8 | impl Tokenizer { 9 | pub fn new(str: &str) -> Tokenizer { 10 | Tokenizer { 11 | str: str.chars().collect(), 12 | pos: 0, 13 | } 14 | } 15 | 16 | pub fn char_code_at(&self, pos: usize) -> char { 17 | if pos < self.str.len() { 18 | self.str[pos] 19 | } else { 20 | '\0' 21 | } 22 | } 23 | 24 | pub fn char_code(&self) -> char { 25 | self.char_code_at(self.pos) 26 | } 27 | 28 | pub fn next_char_code(&self) -> char { 29 | self.char_code_at(self.pos + 1) 30 | } 31 | 32 | pub fn next_non_ws_code(&self, pos: usize) -> char { 33 | self.char_code_at(self.find_ws_end(pos)) 34 | } 35 | 36 | pub fn find_ws_end(&self, pos: usize) -> usize { 37 | self.str 38 | .iter() 39 | .skip(pos) 40 | .position(|c| !matches!(c, '\r' | '\n' | '\u{c}' | ' ' | '\t')) 41 | .map(|p| pos + p) 42 | .unwrap_or(self.str.len()) 43 | } 44 | 45 | pub fn substring_to_pos(&mut self, end: usize) -> String { 46 | let substring = self 47 | .str 48 | .iter() 49 | .skip(self.pos) 50 | .take(end - self.pos) 51 | .collect(); 52 | self.pos = end; 53 | substring 54 | } 55 | 56 | pub fn eat(&mut self, code: char) -> Result<(), SyntaxDefinitionError> { 57 | if self.char_code() != code { 58 | return Err(SyntaxDefinitionError::ParseErrorExpected(code)); 59 | } 60 | 61 | self.pos += 1; 62 | Ok(()) 63 | } 64 | 65 | pub fn peek(&mut self) -> char { 66 | if self.pos < self.str.len() { 67 | let ch = self.str[self.pos]; 68 | self.pos += 1; 69 | ch 70 | } else { 71 | '\0' 72 | } 73 | } 74 | 75 | pub fn error(&self, message: &str) { 76 | eprintln!("Tokenizer error: {message}"); 77 | panic!("Tokenizer error: {message}"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/list_subpages_for_sidebar.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::subpages::{SubPagesSorter, get_sub_pages}; 6 | use crate::html::links::{LinkModifier, render_internal_link}; 7 | use crate::pages::page::{Page, PageLike}; 8 | use crate::utils::{trim_after, trim_before}; 9 | 10 | /// List sub pages for sidebar 11 | #[rari_f(register = "crate::Templ")] 12 | pub fn listsubpagesforsidebar( 13 | url: String, 14 | no_code: Option, 15 | include_parent: Option, 16 | title_only_after: Option, 17 | title_only_before: Option, 18 | ) -> Result { 19 | let mut out = String::new(); 20 | let include_parent = include_parent.map(|i| i.as_bool()).unwrap_or_default(); 21 | let mut sub_pages = get_sub_pages(&url, Some(1), SubPagesSorter::Title)?; 22 | if sub_pages.is_empty() && !include_parent { 23 | return Ok(out); 24 | } 25 | let code = !no_code.map(|b| b.as_bool()).unwrap_or_default(); 26 | if include_parent { 27 | let parent = Page::from_url_with_fallback(&url)?; 28 | sub_pages.insert(0, parent); 29 | } 30 | 31 | out.push_str("
    "); 32 | for page in sub_pages { 33 | let locale_page = if env.locale != Default::default() { 34 | &Page::from_url_with_locale_and_fallback(page.url(), env.locale)? 35 | } else { 36 | &page 37 | }; 38 | let title = locale_page.short_title().unwrap_or(locale_page.title()); 39 | let title = trim_before(title, title_only_after.as_deref()); 40 | let title = trim_after(title, title_only_before.as_deref()); 41 | render_internal_link( 42 | &mut out, 43 | locale_page.url(), 44 | None, 45 | &html_escape::encode_safe(title), 46 | None, 47 | &LinkModifier { 48 | badges: page.status(), 49 | badge_locale: env.locale, 50 | code, 51 | only_en_us: locale_page.locale() != env.locale, 52 | }, 53 | true, 54 | )?; 55 | out.push_str(""); 56 | } 57 | out.push_str("
"); 58 | 59 | Ok(out) 60 | } 61 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/htmlxref.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | 4 | use crate::error::DocError; 5 | use crate::templ::api::RariApi; 6 | 7 | /// Creates a link to an HTML element reference page on MDN. 8 | /// 9 | /// This macro generates links to HTML element documentation. It automatically 10 | /// formats the display text with angle brackets (e.g., `
`) unless custom 11 | /// display text is provided, and applies code formatting by default. 12 | /// 13 | /// # Arguments 14 | /// * `element_name` - The HTML element name (without angle brackets) 15 | /// * `display` - Optional custom display text for the link 16 | /// * `anchor` - Optional anchor/fragment to append to the URL 17 | /// * `_` - Unused parameter (kept for compatibility) 18 | /// 19 | /// # Examples 20 | /// * `{{HTMLElement("div")}}` -> links to `
` element with code formatting 21 | /// * `{{HTMLElement("input", "input element")}}` -> custom display text 22 | /// * `{{HTMLElement("form", "", "#attributes")}}` -> links to form element with anchor 23 | /// 24 | /// # Special handling 25 | /// - Automatically wraps element name in `<` and `>` for display 26 | /// - Uses code formatting (`` tags) by default unless custom display text provided 27 | /// - Links to `/Web/HTML/Reference/Elements/{element_name}` path structure 28 | #[rari_f(register = "crate::Templ")] 29 | pub fn htmlelement( 30 | element_name: String, 31 | display: Option, 32 | anchor: Option, 33 | _: Option, 34 | ) -> Result { 35 | let display = display.filter(|s| !s.is_empty()); 36 | let mut code = false; 37 | let display = display.unwrap_or_else(|| { 38 | code = true; 39 | format!("<{element_name}>") 40 | }); 41 | let mut url = format!( 42 | "/{}/docs/Web/HTML/Reference/Elements/{}", 43 | env.locale.as_url_str(), 44 | element_name, 45 | ); 46 | if let Some(anchor) = anchor { 47 | if !anchor.starts_with('#') { 48 | url.push('#'); 49 | } 50 | url.push_str(&anchor); 51 | } 52 | 53 | RariApi::link( 54 | &url, 55 | Some(env.locale), 56 | Some(display.as_ref()), 57 | code, 58 | None, 59 | false, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/jsfiddle_embed.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_types::AnyArg; 5 | 6 | use crate::error::DocError; 7 | 8 | /// Embeds a JSFiddle code example in an iframe. 9 | /// 10 | /// This macro creates an iframe that displays interactive code examples from JSFiddle, 11 | /// allowing users to view and experiment with HTML, CSS, and JavaScript code directly 12 | /// in the browser. The embed supports various display options and custom sizing. 13 | /// 14 | /// # Arguments 15 | /// * `url` - Base JSFiddle URL (e.g., "https://jsfiddle.net/username/fiddle_id") 16 | /// * `options` - Optional display options for the embed (e.g., "js,html,css,result" or "result") 17 | /// * `height` - Optional height for the iframe (in pixels) 18 | /// 19 | /// # Examples 20 | /// * `{{JSFiddleEmbed("https://jsfiddle.net/user/abc123")}}` -> basic JSFiddle embed 21 | /// * `{{JSFiddleEmbed("https://jsfiddle.net/user/abc123", "result", "400")}}` -> shows only result with custom height 22 | /// * `{{JSFiddleEmbed("https://jsfiddle.net/user/abc123", "js,result")}}` -> shows JavaScript and result tabs 23 | /// 24 | /// # Special handling 25 | /// - Automatically appends "/embedded/" to the JSFiddle URL for proper embedding 26 | /// - Supports JSFiddle's tab options (js, html, css, result) in the options parameter 27 | /// - Uses standard width (756px) with customizable height 28 | /// - Includes allowfullscreen attribute for better user experience 29 | /// - Wraps iframe in paragraph tags for proper content flow 30 | #[rari_f(register = "crate::Templ")] 31 | pub fn jsfiddleembed( 32 | url: String, 33 | options: Option, 34 | height: Option, 35 | ) -> Result { 36 | let mut out = String::new(); 37 | out.push_str(r#"

"#, 52 | ]); 53 | Ok(out) 54 | } 55 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/web_ext_examples.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::sync::LazyLock; 3 | 4 | use rari_types::globals::data_dir; 5 | use rari_utils::io::read_to_string; 6 | use serde::Deserialize; 7 | use tracing::warn; 8 | 9 | use crate::error::DocError; 10 | 11 | #[derive(Deserialize, Debug, Clone)] 12 | pub struct WebExtExample { 13 | pub description: String, 14 | pub javascript_apis: Vec, 15 | pub name: String, 16 | } 17 | 18 | pub struct WebExtExamplesData { 19 | pub by_module: HashMap<&'static str, Vec<&'static WebExtExample>>, 20 | pub by_module_and_api: HashMap<&'static str, Vec<&'static WebExtExample>>, 21 | } 22 | 23 | pub static WEB_EXT_EXAMPLES_JSON: LazyLock> = LazyLock::new(|| { 24 | match read_to_string(data_dir().join("web_ext_examples/data.json")) 25 | .map_err(DocError::from) 26 | .and_then(|s| serde_json::from_str(&s).map_err(DocError::from)) 27 | { 28 | Ok(data) => data, 29 | Err(e) => { 30 | warn!("Error loading mdn/data: {e}"); 31 | Default::default() 32 | } 33 | } 34 | }); 35 | 36 | pub fn web_ext_examples_json() -> &'static [WebExtExample] { 37 | &WEB_EXT_EXAMPLES_JSON 38 | } 39 | 40 | pub static WEB_EXT_EXAMPLES_DATA: LazyLock = LazyLock::new(|| { 41 | let mut by_module = HashMap::new(); 42 | for example in web_ext_examples_json() { 43 | for js_api in &example 44 | .javascript_apis 45 | .iter() 46 | .map(|js_api| &js_api[..js_api.find('.').unwrap_or(js_api.len())]) 47 | .collect::>() 48 | { 49 | by_module 50 | .entry(*js_api) 51 | .and_modify(|e: &mut Vec<_>| e.push(example)) 52 | .or_insert(vec![example]); 53 | } 54 | } 55 | let mut by_module_and_api = HashMap::new(); 56 | for example in web_ext_examples_json() { 57 | for js_api in &example.javascript_apis { 58 | by_module_and_api 59 | .entry(js_api.as_str()) 60 | .and_modify(|e: &mut Vec<_>| e.push(example)) 61 | .or_insert(vec![example]); 62 | } 63 | } 64 | WebExtExamplesData { 65 | by_module, 66 | by_module_and_api, 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /crates/rari-md/src/dl.rs: -------------------------------------------------------------------------------- 1 | use comrak::nodes::{AstNode, NodeValue}; 2 | 3 | pub(crate) fn is_dl<'a>(list: &'a AstNode<'a>) -> bool { 4 | list.children().all(|child| { 5 | if child.children().count() < 2 { 6 | return false; 7 | } 8 | let last_child = child.last_child().unwrap(); 9 | if !matches!(last_child.data.borrow().value, NodeValue::List(_)) { 10 | return false; 11 | } 12 | last_child.children().all(|item| { 13 | if let Some(i) = item.first_child() { 14 | if !matches!(i.data.borrow().value, NodeValue::Paragraph) { 15 | return false; 16 | } 17 | if let Some(j) = i.first_child() 18 | && let NodeValue::Text(ref t) = j.data.borrow().value 19 | { 20 | return t.starts_with(": "); 21 | } 22 | } 23 | false 24 | }) 25 | }) 26 | } 27 | 28 | pub(crate) fn convert_dl<'a>(list: &'a AstNode<'a>) { 29 | list.data.borrow_mut().value = NodeValue::DescriptionList; 30 | for child in list.children() { 31 | child.data.borrow_mut().value = NodeValue::DescriptionTerm; 32 | let last_child = child.last_child().unwrap(); 33 | if !matches!(last_child.data.borrow().value, NodeValue::List(_)) { 34 | continue; 35 | } 36 | last_child.detach(); 37 | for item in last_child.reverse_children() { 38 | if let Some(i) = item.first_child() { 39 | if !matches!(i.data.borrow().value, NodeValue::Paragraph) { 40 | break; 41 | } 42 | if let Some(j) = i.first_child() 43 | && let NodeValue::Text(ref mut t) = j.data.borrow_mut().value 44 | { 45 | match t.len() { 46 | 0 => {} 47 | 1 => { 48 | t.drain(0..1); 49 | } 50 | _ => { 51 | t.drain(0..2); 52 | } 53 | } 54 | } 55 | } 56 | item.data.borrow_mut().value = NodeValue::DescriptionDetails; 57 | item.detach(); 58 | child.insert_after(item); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/embeds/interactive_example.rs: -------------------------------------------------------------------------------- 1 | use html_escape::encode_double_quoted_attribute; 2 | use rari_templ_func::rari_f; 3 | use rari_utils::concat_strs; 4 | 5 | use crate::error::DocError; 6 | use crate::helpers::l10n::l10n_json_data; 7 | use crate::templ::api::RariApi; 8 | 9 | /// Creates an interactive example element for hands-on code demonstrations. 10 | /// 11 | /// This macro generates an `` custom element that provides 12 | /// interactive code examples for learning web technologies. It creates a localized 13 | /// section heading and embeds the interactive component with optional height customization. 14 | /// 15 | /// # Arguments 16 | /// * `name` - Descriptive name of the interactive example (displayed in heading) 17 | /// * `height` - Optional height class for the interactive element ("shorter", "taller", etc.) 18 | /// 19 | /// # Examples 20 | /// * `{{InteractiveExample("JavaScript Demo: Array.from()")}}` -> basic interactive example 21 | /// * `{{InteractiveExample("CSS Flexbox Layout", "taller")}}` -> with custom height 22 | /// * `{{InteractiveExample("Web API: Fetch", "shorter")}}` -> with shorter height 23 | /// 24 | /// # Special handling 25 | /// - Generates localized "Try it" section heading with proper anchor ID 26 | /// - HTML-escapes the example name for security and proper display 27 | /// - Creates accessible heading structure for screen readers 28 | /// - Supports custom height classes for different types of content 29 | /// - Uses semantic HTML with proper heading hierarchy 30 | #[rari_f(register = "crate::Templ")] 31 | pub fn interactiveexample(name: String, height: Option) -> Result { 32 | let title = l10n_json_data("Template", "interactive_example_cta", env.locale)?; 33 | let id = RariApi::anchorize(title); 34 | 35 | let height = height 36 | .map(|height| { 37 | concat_strs!( 38 | r#" height=""#, 39 | &encode_double_quoted_attribute(&height).as_ref(), 40 | r#"""# 41 | ) 42 | }) 43 | .unwrap_or_default(); 44 | Ok(concat_strs!( 45 | r#"

"#, 48 | title, 49 | "

\n", 50 | r#""# 55 | )) 56 | } 57 | -------------------------------------------------------------------------------- /crates/rari-doc/src/walker.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use ignore::WalkBuilder; 4 | use ignore::types::TypesBuilder; 5 | use rari_types::globals::{content_root, content_translated_root, settings}; 6 | 7 | /// Creates a `WalkBuilder` for walking through the specified paths globbing "index.md" files. The glob can be overridden. 8 | /// 9 | /// This function initializes a `WalkBuilder` for traversing the file system starting from the given paths. 10 | /// It supports optional glob patterns to filter the files being walked, but by default, it looks for "index.md" files. 11 | /// For running in the test environment, the function also configures the `WalkBuilder` to respect or ignore `.gitignore` 12 | /// files based on the settings `reader_ignores_gitignore`. Testing usually does not follow `.gitignore` files' rules. 13 | /// 14 | /// # Arguments 15 | /// 16 | /// * `paths` - A slice of paths to start the walk from. Each path should implement `AsRef`. 17 | /// * `glob` - An optional string slice that holds the glob pattern to filter the files. If `None`, defaults to "index.md". 18 | /// 19 | /// # Returns 20 | /// 21 | /// * `Result` - Returns a configured `WalkBuilder` if successful, 22 | /// or an `ignore::Error` if an error occurs while building the file types. 23 | /// 24 | /// # Errors 25 | /// 26 | /// This function will return an error if: 27 | /// - An error occurs while adding the glob pattern to the `TypesBuilder`. 28 | /// - An error occurs while building the file types. 29 | pub(crate) fn walk_builder( 30 | paths: &[impl AsRef], 31 | glob: Option<&str>, 32 | ) -> Result { 33 | let mut types = TypesBuilder::new(); 34 | types.add_def(&format!("markdown:{}", glob.unwrap_or("index.md")))?; 35 | types.select("markdown"); 36 | let mut paths_iter = paths.iter(); 37 | let mut builder = if let Some(path) = paths_iter.next() { 38 | let mut builder = ignore::WalkBuilder::new(path); 39 | for path in paths_iter { 40 | builder.add(path); 41 | } 42 | builder 43 | } else { 44 | let mut builder = ignore::WalkBuilder::new(content_root()); 45 | if let Some(root) = content_translated_root() { 46 | builder.add(root); 47 | } 48 | builder 49 | }; 50 | builder.git_ignore(!settings().reader_ignores_gitignore); 51 | builder.types(types.build()?); 52 | Ok(builder) 53 | } 54 | -------------------------------------------------------------------------------- /crates/css-syntax-types/src/specs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::BTreeMap; 3 | 4 | #[derive(Debug, Clone, Deserialize, Serialize)] 5 | pub struct BrowserSpec { 6 | pub url: String, 7 | #[serde(rename = "seriesComposition")] 8 | pub series_composition: String, 9 | pub shortname: String, 10 | pub series: Series, 11 | #[serde(rename = "seriesVersion")] 12 | pub series_version: Option, 13 | #[serde(rename = "formerNames", default)] 14 | pub former_names: Vec, 15 | pub nightly: Option, 16 | pub title: String, 17 | #[serde(rename = "shortTitle")] 18 | pub short_title: String, 19 | pub organization: String, 20 | pub groups: Vec, 21 | pub release: Option, 22 | pub source: String, 23 | pub categories: Vec, 24 | pub standing: String, 25 | pub tests: Option, 26 | #[serde(flatten)] 27 | pub extra: BTreeMap, // For any additional fields 28 | } 29 | 30 | #[derive(Debug, Clone, Deserialize, Serialize)] 31 | pub struct Series { 32 | pub shortname: String, 33 | #[serde(rename = "currentSpecification")] 34 | pub current_specification: String, 35 | pub title: String, 36 | #[serde(rename = "shortTitle")] 37 | pub short_title: String, 38 | #[serde(rename = "releaseUrl")] 39 | pub release_url: Option, 40 | #[serde(rename = "nightlyUrl")] 41 | pub nightly_url: Option, 42 | } 43 | 44 | #[derive(Debug, Clone, Deserialize, Serialize)] 45 | pub struct NightlySpec { 46 | pub url: String, 47 | pub status: String, 48 | #[serde(rename = "sourcePath")] 49 | pub source_path: Option, 50 | #[serde(rename = "alternateUrls", default)] 51 | pub alternate_urls: Vec, 52 | pub repository: Option, 53 | pub filename: String, 54 | } 55 | 56 | #[derive(Debug, Clone, Deserialize, Serialize)] 57 | pub struct ReleaseSpec { 58 | pub url: String, 59 | pub status: String, 60 | pub pages: Option>, 61 | pub filename: String, 62 | } 63 | 64 | #[derive(Debug, Clone, Deserialize, Serialize)] 65 | pub struct Group { 66 | pub name: String, 67 | pub url: String, 68 | } 69 | 70 | #[derive(Debug, Clone, Deserialize, Serialize)] 71 | pub struct TestInfo { 72 | pub repository: String, 73 | #[serde(rename = "testPaths")] 74 | pub test_paths: Vec, 75 | } 76 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/xsltref.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::locale::Locale; 3 | use rari_utils::concat_strs; 4 | 5 | use crate::error::DocError; 6 | 7 | #[rari_f(register = "crate::Templ")] 8 | pub fn xsltref() -> Result { 9 | Ok(concat_strs!( 10 | r#"
"#, 11 | match env.locale { 12 | Locale::Es => 13 | r#"Referencia de XSLT y XPath: Elementos XSLT, Funciones EXSLT, XPath:Funciones, XPath:Ejes"#, 14 | Locale::Fr => 15 | r#"Référence XSLT/XPath : Éléments XSLT, Fonctions EXSLT, Fonctions XPath, Axes XPath"#, 16 | Locale::Ja => 17 | r#"XSLT/XPath リファレンス: XSLT 要素, EXSLT 関数, XPath 関数, XPath 軸"#, 18 | Locale::Ko => 19 | r#"XSLT/XPath 참고 문서: XSLT 요소, XPath 함수, XPath 축"#, 20 | Locale::ZhCn => 21 | r#"XSLT/XPath 参考XSLT 元素EXSLT 函数XPath 函数XPath 轴"#, 22 | _ => 23 | r#"XSLT/XPath Reference: XSLT elements, EXSLT functions, XPath functions, XPath axes"#, 24 | }, 25 | r#"
"# 26 | )) 27 | } 28 | -------------------------------------------------------------------------------- /crates/rari-tools/src/wikihistory.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fs::{self, File}; 3 | use std::io::{BufWriter, Write}; 4 | use std::path::Path; 5 | 6 | use rari_doc::utils::root_for_locale; 7 | use rari_types::locale::Locale; 8 | use serde_json::Value; 9 | 10 | use crate::error::ToolError; 11 | 12 | pub(crate) fn update_wiki_history( 13 | locale: Locale, 14 | pairs: &[(String, String)], 15 | ) -> Result<(), ToolError> { 16 | let mut all = read_wiki_history(locale)?; 17 | for (old_slug, new_slug) in pairs { 18 | if let Some(to) = all.remove(old_slug) { 19 | all.insert(new_slug.to_string(), to); 20 | } 21 | } 22 | write_wiki_history(locale, all)?; 23 | Ok(()) 24 | } 25 | 26 | pub(crate) fn delete_from_wiki_history(locale: Locale, slugs: &[String]) -> Result<(), ToolError> { 27 | let mut all = read_wiki_history(locale)?; 28 | for slug in slugs { 29 | all.remove(slug); 30 | } 31 | write_wiki_history(locale, all)?; 32 | Ok(()) 33 | } 34 | 35 | fn write_wiki_history(locale: Locale, all: BTreeMap) -> Result<(), ToolError> { 36 | let wiki_history_path = wiki_history_path(locale)?; 37 | let file = File::create(&wiki_history_path)?; 38 | let mut buffer = BufWriter::new(file); 39 | // Write the updated pretty JSON back to the file 40 | serde_json::to_writer_pretty(&mut buffer, &all)?; 41 | // Add a trailing newline 42 | buffer.write_all(b"\n")?; 43 | Ok(()) 44 | } 45 | 46 | pub(crate) fn read_wiki_history(locale: Locale) -> Result, ToolError> { 47 | let wiki_history_path = wiki_history_path(locale)?; 48 | // Read the content of the JSON file 49 | let wiki_history_content = fs::read_to_string(&wiki_history_path)?; 50 | // Parse the JSON content into a BTreeMap (sorted map) 51 | let all: BTreeMap = serde_json::from_str(&wiki_history_content)?; 52 | Ok(all) 53 | } 54 | 55 | fn wiki_history_path(locale: Locale) -> Result { 56 | let locale_content_root = root_for_locale(locale)?; 57 | Ok(Path::new(locale_content_root) 58 | .join(locale.as_folder_str()) 59 | .join("_wikihistory.json") 60 | .to_string_lossy() 61 | .to_string()) 62 | } 63 | 64 | #[cfg(test)] 65 | pub(crate) fn test_get_wiki_history(locale: Locale) -> BTreeMap { 66 | read_wiki_history(locale).expect("Could not read wiki history") 67 | } 68 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/rfc.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::l10n::l10n_json_data; 6 | 7 | /// Creates a link to an IETF RFC (Request for Comments) document. 8 | /// 9 | /// This macro generates links to RFC documents hosted on the IETF datatracker. 10 | /// It supports linking to specific sections within an RFC and can include 11 | /// custom descriptive content in the link text. 12 | /// 13 | /// # Arguments 14 | /// * `number` - The RFC number (e.g., 7231, 3986, 2616) 15 | /// * `content` - Optional descriptive content to append to the link text 16 | /// * `anchor` - Optional section number to link to a specific section 17 | /// 18 | /// # Examples 19 | /// * `{{RFC("7231")}}` -> links to "RFC 7231" 20 | /// * `{{RFC("3986", "URI Generic Syntax")}}` -> links to "RFC 3986: URI Generic Syntax" 21 | /// * `{{RFC("7231", "", "6.1")}}` -> links to "RFC 7231, section 6.1" with anchor 22 | /// * `{{RFC("2616", "HTTP/1.1", "14.9")}}` -> links to "RFC 2616, section 14.9: HTTP/1.1" 23 | /// 24 | /// # Special handling 25 | /// - Links directly to https://datatracker.ietf.org/doc/html/rfc{number} 26 | /// - Section anchors are formatted as `#section-{anchor}` 27 | /// - Localizes the word "section" based on the current locale 28 | /// - Combines content and section information intelligently in link text 29 | #[rari_f(register = "crate::Templ")] 30 | pub fn rfc( 31 | number: AnyArg, 32 | content: Option, 33 | anchor: Option, 34 | ) -> Result { 35 | let (content, anchor): (String, String) = match (content, anchor) { 36 | (None, None) => Default::default(), 37 | (None, Some(anchor)) => ( 38 | format!( 39 | ", {} {anchor}", 40 | l10n_json_data("Common", "section", env.locale)? 41 | ), 42 | format!("#section-{anchor}"), 43 | ), 44 | (Some(content), None) => (format!(": {content}"), Default::default()), 45 | (Some(content), Some(anchor)) => ( 46 | format!( 47 | ", {} {anchor}: {content}", 48 | l10n_json_data("Common", "section", env.locale)? 49 | ), 50 | format!("#section-{anchor}"), 51 | ), 52 | }; 53 | let number = number.as_int(); 54 | Ok(format!( 55 | r#"RFC {number}{content}"# 56 | )) 57 | } 58 | -------------------------------------------------------------------------------- /crates/rari-tools/src/tests/fixtures/redirects.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use rari_doc::utils::root_for_locale; 5 | use rari_types::locale::Locale; 6 | 7 | #[derive(Debug)] 8 | pub(crate) struct RedirectFixtures { 9 | pub path: PathBuf, 10 | do_not_remove: bool, 11 | } 12 | 13 | impl RedirectFixtures { 14 | pub fn new(entries: &[(String, String)], locale: Locale) -> Self { 15 | Self::new_internal(entries, locale, false) 16 | } 17 | #[allow(dead_code)] 18 | pub fn debug_new(entries: &[(String, String)], locale: Locale) -> Self { 19 | Self::new_internal(entries, locale, true) 20 | } 21 | 22 | pub fn all_locales_empty() -> Vec { 23 | Locale::for_generic_and_spas() 24 | .iter() 25 | .map(|locale| Self::new_internal(&[], *locale, false)) 26 | .collect() 27 | } 28 | #[allow(dead_code)] 29 | pub fn debug_all_locales_empty() -> Vec { 30 | Locale::for_generic_and_spas() 31 | .iter() 32 | .map(|locale| Self::new_internal(&[], *locale, true)) 33 | .collect() 34 | } 35 | 36 | fn new_internal(entries: &[(String, String)], locale: Locale, do_not_remove: bool) -> Self { 37 | // create wiki history file for each slug in the vector, in the configured root directory for the locale 38 | let mut folder_path = PathBuf::new(); 39 | folder_path.push(root_for_locale(locale).unwrap()); 40 | folder_path.push(locale.as_folder_str()); 41 | fs::create_dir_all(&folder_path).unwrap(); 42 | folder_path.push("_redirects.txt"); 43 | 44 | let mut content = String::new(); 45 | for (from, to) in entries { 46 | content.push_str( 47 | format!( 48 | "/{}/{}\t/{}/{}\n", 49 | locale.as_url_str(), 50 | from, 51 | locale.as_url_str(), 52 | to 53 | ) 54 | .as_str(), 55 | ); 56 | } 57 | content.push('\n'); 58 | fs::write(&folder_path, content).unwrap(); 59 | 60 | RedirectFixtures { 61 | path: folder_path, 62 | do_not_remove, 63 | } 64 | } 65 | } 66 | 67 | impl Drop for RedirectFixtures { 68 | fn drop(&mut self) { 69 | if self.do_not_remove { 70 | tracing::info!( 71 | "Leaving redirects fixture {} in place for debugging", 72 | self.path.display() 73 | ); 74 | return; 75 | } 76 | 77 | fs::remove_file(&self.path).ok(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/rari-tools/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::path::PathBuf; 3 | 4 | use rari_doc::error::{DocError, UrlError}; 5 | use rari_types::error::EnvError; 6 | use rari_types::locale::LocaleError; 7 | use rari_utils::error::RariIoError; 8 | use thiserror::Error; 9 | 10 | #[derive(Debug, Error)] 11 | pub enum ToolError { 12 | #[error("Invalid slug: {0}")] 13 | InvalidSlug(Cow<'static, str>), 14 | #[error("Invalid url: {0}")] 15 | InvalidUrl(Cow<'static, str>), 16 | #[error("Invalid locale: {0}")] 17 | InvalidLocale(Cow<'static, str>), 18 | #[error("Orphaned doc exists: {0}")] 19 | OrphanedDocExists(Cow<'static, str>), 20 | #[error("Git error: {0}")] 21 | GitError(String), 22 | 23 | #[error(transparent)] 24 | LocaleError(#[from] LocaleError), 25 | #[error(transparent)] 26 | DocError(#[from] DocError), 27 | #[error(transparent)] 28 | EnvError(#[from] EnvError), 29 | #[error(transparent)] 30 | UrlError(#[from] UrlError), 31 | #[error(transparent)] 32 | IoError(#[from] std::io::Error), 33 | #[error(transparent)] 34 | RariIoError(#[from] RariIoError), 35 | #[error(transparent)] 36 | JsonError(#[from] serde_json::Error), 37 | #[error(transparent)] 38 | YamlError(#[from] yaml_parser::SyntaxError), 39 | #[error("Invalid Redirection: {0}")] 40 | InvalidRedirectionEntry(String), 41 | #[error("Error reading redirects file: {0}")] 42 | ReadRedirectsError(String), 43 | #[error("Error writing redirects file: {0}")] 44 | WriteRedirectsError(String), 45 | #[error("Invalid 'from' URL for redirect: {0}")] 46 | InvalidRedirectFromURL(String), 47 | #[error("Invalid 'to' URL for redirect: {0}")] 48 | InvalidRedirectToURL(String), 49 | #[error("Invalid redirects: not in alphabetical order: {0} -> {1} before {2} -> {3}")] 50 | InvalidRedirectOrder(String, String, String, String), 51 | #[error("Invalid redirect for {0} -> {1} or {2} -> {3}")] 52 | InvalidRedirect(String, String, String, String), 53 | #[error(transparent)] 54 | RedirectError(#[from] RedirectError), 55 | #[error("Invalid yaml {0}")] 56 | InvalidFrontmatter(#[from] serde_yaml_ng::Error), 57 | #[error("Page has subpages: {0}")] 58 | HasSubpagesError(Cow<'static, str>), 59 | #[error("Target directory ({0}) for slug ({1}) already exists")] 60 | TargetDirExists(PathBuf, String), 61 | 62 | #[error("Unknown error")] 63 | Unknown(&'static str), 64 | } 65 | 66 | #[derive(Debug, Clone, Error)] 67 | pub enum RedirectError { 68 | #[error("RedirectError: {0}")] 69 | Cycle(String), 70 | #[error("No cased version {0}")] 71 | NoCased(String), 72 | } 73 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/l10n.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::sync::LazyLock; 4 | 5 | use rari_types::globals::content_root; 6 | use rari_types::locale::Locale; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, PartialEq, Clone, Error)] 10 | pub enum L10nError { 11 | #[error("Invalid key for L10n json data: {0}")] 12 | InvalidKey(String), 13 | #[error("EnUS missing in L10n json data")] 14 | NoEnUs, 15 | } 16 | 17 | // Look up a translation from mdn/content's `jsondata folder. 18 | // `typ` refers to the `L10n-.json` file. 19 | pub fn l10n_json_data(typ: &str, key: &str, locale: Locale) -> Result<&'static str, L10nError> { 20 | if let Some(data) = JSON_L10N_FILES.get(typ).and_then(|file| file.get(key)) { 21 | get_for_locale(locale, data) 22 | .map(|s| s.as_str()) 23 | .ok_or(L10nError::NoEnUs) 24 | } else { 25 | Err(L10nError::InvalidKey(key.to_string())) 26 | } 27 | } 28 | 29 | fn get_for_locale(locale: Locale, lookup: &HashMap) -> Option<&T> { 30 | if let Some(value) = lookup.get(locale.as_url_str()) { 31 | Some(value) 32 | } else if locale != Locale::default() { 33 | lookup.get(Locale::default().as_url_str()) 34 | } else { 35 | None 36 | } 37 | } 38 | pub type JsonL10nFile = HashMap>; 39 | 40 | static JSON_L10N_FILES: LazyLock> = LazyLock::new(|| { 41 | content_root() 42 | .join("jsondata") 43 | .read_dir() 44 | .expect("unable to read jsondata dir") 45 | .filter_map(|f| { 46 | if let Ok(f) = f 47 | && f.path().is_file() 48 | && f.path() 49 | .extension() 50 | .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) 51 | && f.path() 52 | .file_stem() 53 | .and_then(|s| s.to_str()) 54 | .is_some_and(|s| s.starts_with("L10n-")) 55 | { 56 | return Some(f.path()); 57 | } 58 | None 59 | }) 60 | .map(|f| { 61 | let typ = f 62 | .file_stem() 63 | .and_then(|s| s.to_str()) 64 | .unwrap_or_default() 65 | .strip_prefix("L10n-") 66 | .unwrap_or_default(); 67 | let json_str = fs::read_to_string(&f).expect("unable to read l10n json"); 68 | let l10n_json: JsonL10nFile = 69 | serde_json::from_str(&json_str).expect("unable to parse l10n json"); 70 | (typ.into(), l10n_json) 71 | }) 72 | .collect() 73 | }); 74 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/webextapixref.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_types::AnyArg; 5 | 6 | use crate::error::DocError; 7 | use crate::templ::api::RariApi; 8 | 9 | /// Creates a link to a WebExtensions API reference page on MDN. 10 | /// 11 | /// This macro generates links to WebExtensions (browser extension) API documentation. 12 | /// It handles various API naming conventions including namespaces, methods, and properties, 13 | /// and can automatically format display text and anchors for nested API references. 14 | /// 15 | /// # Arguments 16 | /// * `api` - The WebExtensions API name (namespace, method, property, etc.) 17 | /// * `display` - Optional custom display text for the link 18 | /// * `anchor` - Optional anchor/fragment to append to the URL 19 | /// * `no_code` - Optional flag to disable code formatting (default: false) 20 | /// 21 | /// # Examples 22 | /// * `{{WebExtAPIRef("tabs")}}` -> links to tabs API namespace 23 | /// * `{{WebExtAPIRef("tabs.query")}}` -> links to tabs.query method 24 | /// * `{{WebExtAPIRef("runtime.onMessage", "onMessage event")}}` -> custom display text 25 | /// * `{{WebExtAPIRef("storage.local", "", "#get")}}` -> with anchor to specific method 26 | /// * `{{WebExtAPIRef("alarms", "", "", true)}}` -> disables code formatting 27 | /// 28 | /// # Special handling 29 | /// - Converts spaces to underscores and removes `()` from method names for URLs 30 | /// - Handles dot notation (`.`) by converting to `/` for URL paths 31 | /// - Appends anchor information to display text when anchors are used 32 | /// - Formats links with `` tags unless `no_code` is true 33 | /// - Links to `/Mozilla/Add-ons/WebExtensions/API/{api}` path structure 34 | #[rari_f(register = "crate::Templ")] 35 | pub fn webextapiref( 36 | api: String, 37 | display: Option, 38 | anchor: Option, 39 | no_code: Option, 40 | ) -> Result { 41 | let display = display.as_deref().filter(|s| !s.is_empty()); 42 | let mut display = display.map(Cow::Borrowed).unwrap_or(Cow::Borrowed(&api)); 43 | let mut url = format!( 44 | "/{}/docs/Mozilla/Add-ons/WebExtensions/API/{}", 45 | env.locale.as_url_str(), 46 | &api.replace(' ', "_").replace("()", "").replace('.', "/"), 47 | ); 48 | if let Some(anchor) = anchor { 49 | if !anchor.starts_with('#') { 50 | url.push('#'); 51 | } 52 | url.push_str(&anchor); 53 | display.to_mut().push('#'); 54 | display.to_mut().push_str(&anchor); 55 | }; 56 | 57 | RariApi::link( 58 | &url, 59 | Some(env.locale), 60 | Some(display.as_ref()), 61 | !no_code.map(|nc| nc.as_bool()).unwrap_or_default(), 62 | None, 63 | false, 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /crates/rari-doc/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Rari Documentation System 2 | //! 3 | //! The `rari_doc` crate is the central crate of the `rari` build system. It provides a robust build pipeline 4 | //! and various utilities to handle different aspects of the documentation pipeline, including reading, 5 | //! parsing, and rendering pages. 6 | //! 7 | //! ## Modules 8 | //! 9 | //! - `baseline`: Handles baseline configurations and settings. 10 | //! - `build`: Manages the build process for the documentation. 11 | //! - `cached_readers`: Provides cached readers for efficient file access. 12 | //! - `contributors`: Handles generating contributors.txt. 13 | //! - `find`: Search for docs. 14 | //! - `error`: Defines error types used throughout the crate. 15 | //! - `helpers`: Contains helper functions and utilities. 16 | //! - `html`: Manages HTML rendering and processing. 17 | //! - `pages`: Handles the creation and management of documentation pages. 18 | //! - `percent`: Utilities for percent encodings. 19 | //! - `position_utils`: Utilities for converting between byte offsets and character positions. 20 | //! - `reader`: Defines traits and implementations for reading pages. 21 | //! - `redirects`: Manages URL redirects within the documentation. 22 | //! - `resolve`: Handles path and URL resolution. 23 | //! - `rss`: Create the blog rss feed. 24 | //! - `search_index`: Manages the search index for the documentation. 25 | //! - `sidebars`: Handles sidebar generation and management. 26 | //! - `specs`: Manages Web-Spec and Browser Compatibility (BCD) data. 27 | //! - `templ`: Handles templating, macros and rendering of pages. 28 | //! - `translations`: Tools for efficiently looking up translated documents. 29 | //! - `utils`: Contains various utility functions. 30 | //! - `walker`: Provides functionality to walk through the documentation file tree. 31 | //! 32 | //! ## Introduction to Rari Pages and Build Pipeline 33 | //! 34 | //! Rari pages are the core components of the documentation system. Each page can be read, 35 | //! parsed, and rendered using the various modules provided 36 | //! by the `rari_doc` crate. The build pipeline is designed to efficiently process these pages, 37 | //! handling tasks such as reading from source files, applying templates, managing translations, 38 | //! and generating the final output. 39 | pub mod baseline; 40 | pub mod build; 41 | pub mod cached_readers; 42 | pub mod contributors; 43 | pub mod error; 44 | pub mod find; 45 | pub mod helpers; 46 | pub mod html; 47 | pub mod issues; 48 | pub mod pages; 49 | pub mod percent; 50 | pub mod position_utils; 51 | pub mod reader; 52 | pub mod redirects; 53 | pub mod resolve; 54 | pub mod rss; 55 | pub mod search_index; 56 | pub mod sidebars; 57 | pub mod specs; 58 | pub mod templ; 59 | pub mod translations; 60 | pub mod utils; 61 | pub mod walker; 62 | 63 | pub use templ::templs::Templ; 64 | -------------------------------------------------------------------------------- /crates/rari-md/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::Write; 3 | 4 | use crate::ctype::isspace; 5 | 6 | pub fn tagfilter(literal: &[u8]) -> bool { 7 | static TAGFILTER_BLACKLIST: [&str; 9] = [ 8 | "title", 9 | "textarea", 10 | "style", 11 | "xmp", 12 | "iframe", 13 | "noembed", 14 | "noframes", 15 | "script", 16 | "plaintext", 17 | ]; 18 | 19 | if literal.len() < 3 || literal[0] != b'<' { 20 | return false; 21 | } 22 | 23 | let mut i = 1; 24 | if literal[i] == b'/' { 25 | i += 1; 26 | } 27 | 28 | let lc = unsafe { String::from_utf8_unchecked(literal[i..].to_vec()) }.to_lowercase(); 29 | for t in TAGFILTER_BLACKLIST.iter() { 30 | if lc.starts_with(t) { 31 | let j = i + t.len(); 32 | return isspace(literal[j]) 33 | || literal[j] == b'>' 34 | || (literal[j] == b'/' && literal.len() >= j + 2 && literal[j + 1] == b'>'); 35 | } 36 | } 37 | 38 | false 39 | } 40 | 41 | pub fn tagfilter_block(input: &[u8], o: &mut dyn Write) -> io::Result<()> { 42 | let size = input.len(); 43 | let mut i = 0; 44 | 45 | while i < size { 46 | let org = i; 47 | while i < size && input[i] != b'<' { 48 | i += 1; 49 | } 50 | 51 | if i > org { 52 | o.write_all(&input[org..i])?; 53 | } 54 | 55 | if i >= size { 56 | break; 57 | } 58 | 59 | if tagfilter(&input[i..]) { 60 | o.write_all(b"<")?; 61 | } else { 62 | o.write_all(b"<")?; 63 | } 64 | 65 | i += 1; 66 | } 67 | 68 | Ok(()) 69 | } 70 | pub fn escape_href(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> { 71 | let size = buffer.len(); 72 | let mut i = 0; 73 | let mut escaped = ""; 74 | 75 | while i < size { 76 | let org = i; 77 | while i < size { 78 | escaped = match buffer[i] { 79 | b'&' => "&", 80 | b'<' => "<", 81 | b'>' => ">", 82 | b'"' => """, 83 | b'\'' => "'", 84 | _ => { 85 | i += 1; 86 | "" 87 | } 88 | }; 89 | if !escaped.is_empty() { 90 | break; 91 | } 92 | } 93 | 94 | if i > org { 95 | output.write_all(&buffer[org..i])?; 96 | } 97 | 98 | if !escaped.is_empty() { 99 | output.write_all(escaped.as_bytes())?; 100 | escaped = ""; 101 | i += 1; 102 | } 103 | } 104 | 105 | Ok(()) 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/_test-content-run.yml: -------------------------------------------------------------------------------- 1 | name: Run Reusable Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | rari-binary-artifact-id: 7 | description: "ID of rari binary artifact" 8 | required: true 9 | type: string 10 | command: 11 | description: 'The rari command to run (e.g., content fix-flaws, content fmt-sidebars)' 12 | required: true 13 | type: string 14 | translated-content: 15 | description: 'Whether to checkout translated-content' 16 | required: false 17 | type: boolean 18 | default: false 19 | diff: 20 | description: Whether to diff changes 21 | required: false 22 | type: boolean 23 | default: false 24 | 25 | permissions: {} # No GITHUB_TOKEN permissions, as we only use it to increase API limit. 26 | 27 | jobs: 28 | run: 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Checkout (content) 33 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 34 | with: 35 | repository: mdn/content 36 | path: content 37 | persist-credentials: false 38 | 39 | - name: Checkout (translated-content) 40 | if: inputs.translated-content 41 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 42 | with: 43 | repository: mdn/translated-content 44 | path: translated-content 45 | persist-credentials: false 46 | 47 | - name: Download rari binary 48 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 49 | with: 50 | artifact-ids: ${{ inputs.rari-binary-artifact-id }} 51 | path: rari/target/release 52 | 53 | - name: Prepare rari binary 54 | run: chmod +x rari/target/release/rari 55 | 56 | - name: Setup translated-content 57 | if: inputs.translated-content 58 | env: 59 | CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/translated-content/files 60 | run: echo "CONTENT_TRANSLATED_ROOT=$CONTENT_TRANSLATED_ROOT" >> "$GITHUB_ENV" 61 | 62 | - name: Run ${{ inputs.command }} 63 | working-directory: rari 64 | env: 65 | CONTENT_ROOT: ${{ github.workspace }}/content/files 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | run: target/release/rari ${{ inputs.command }} 68 | 69 | - name: Diff content 70 | if: inputs.diff 71 | working-directory: content 72 | run: git diff --color=always --word-diff --word-diff-regex='[[:alnum:]_]+' 73 | 74 | - name: Diff translated-content 75 | if: inputs.diff && inputs.translated-content 76 | working-directory: translated-content 77 | run: git diff --color=always --word-diff --word-diff-regex='[[:alnum:]_]+' 78 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/badges.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::locale::Locale; 3 | 4 | use crate::error::DocError; 5 | use crate::helpers::l10n::l10n_json_data; 6 | 7 | #[rari_f(register = "crate::Templ")] 8 | pub fn experimental_inline() -> Result { 9 | let mut out = String::new(); 10 | write_experimental(&mut out, env.locale)?; 11 | Ok(out) 12 | } 13 | 14 | #[rari_f(register = "crate::Templ")] 15 | pub fn experimentalbadge() -> Result { 16 | experimental_inline(env) 17 | } 18 | 19 | #[rari_f(register = "crate::Templ")] 20 | pub fn non_standard_inline() -> Result { 21 | let mut out = String::new(); 22 | write_non_standard(&mut out, env.locale)?; 23 | Ok(out) 24 | } 25 | 26 | #[rari_f(register = "crate::Templ")] 27 | pub fn non_standardbage() -> Result { 28 | non_standard_inline(env) 29 | } 30 | 31 | #[rari_f(register = "crate::Templ")] 32 | pub fn deprecated_inline() -> Result { 33 | let mut out = String::new(); 34 | write_deprecated(&mut out, env.locale)?; 35 | Ok(out) 36 | } 37 | 38 | #[rari_f(register = "crate::Templ")] 39 | pub fn optional_inline() -> Result { 40 | let str = l10n_json_data("Template", "optional", env.locale)?; 41 | Ok(format!( 42 | r#"{str}"# 43 | )) 44 | } 45 | 46 | pub fn write_experimental(out: &mut impl std::fmt::Write, locale: Locale) -> Result<(), DocError> { 47 | let title = l10n_json_data("Template", "experimental_badge_title", locale)?; 48 | let abbreviation = l10n_json_data("Template", "experimental_badge_abbreviation", locale)?; 49 | 50 | Ok(write_badge(out, title, abbreviation, "experimental")?) 51 | } 52 | 53 | pub fn write_non_standard(out: &mut impl std::fmt::Write, locale: Locale) -> Result<(), DocError> { 54 | let title = l10n_json_data("Template", "non_standard_badge_title", locale)?; 55 | let abbreviation = l10n_json_data("Template", "non_standard_badge_abbreviation", locale)?; 56 | 57 | Ok(write_badge(out, title, abbreviation, "nonstandard")?) 58 | } 59 | 60 | pub fn write_deprecated(out: &mut impl std::fmt::Write, locale: Locale) -> Result<(), DocError> { 61 | let title = l10n_json_data("Template", "deprecated_badge_title", locale)?; 62 | let abbreviation = l10n_json_data("Template", "deprecated_badge_abbreviation", locale)?; 63 | 64 | Ok(write_badge(out, title, abbreviation, "deprecated")?) 65 | } 66 | 67 | pub fn write_badge( 68 | out: &mut impl std::fmt::Write, 69 | title: &str, 70 | abbreviation: &str, 71 | typ: &str, 72 | ) -> std::fmt::Result { 73 | let title = html_escape::encode_quoted_attribute(title); 74 | write!( 75 | out, 76 | r#" 77 | {abbreviation} 78 | "# 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/firefox_for_developers.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_types::locale::Locale; 5 | 6 | use crate::error::DocError; 7 | use crate::helpers::l10n::l10n_json_data; 8 | use crate::html::links::{LinkFlags, render_link_via_page}; 9 | 10 | const OLD_VERSIONS: &[&str] = &["3.6", "3.5", "3", "2", "1.5"]; 11 | 12 | #[rari_f(register = "crate::Templ")] 13 | pub fn firefox_for_developers() -> Result { 14 | let locale = env.locale; 15 | let slug = env.slug; 16 | 17 | let version_str = slug 18 | .split('/') 19 | .next_back() 20 | .ok_or_else(|| invalid_slug(slug))?; 21 | 22 | // Determine if version_str is a float (for OLD_VERSIONS) or integer 23 | let mut max_version: i32 = if let Ok(int_version) = version_str.parse::() { 24 | int_version 25 | } else if OLD_VERSIONS.contains(&version_str) { 26 | // If version_str is in OLD_VERSIONS, treat it as 3 (since all are < 4) 27 | 3 28 | } else { 29 | return Err(invalid_slug(slug)); 30 | }; 31 | 32 | let mut old_version_start_idx = 0; 33 | 34 | // Determine the start index of old version and the max version 35 | if max_version < 4 { 36 | old_version_start_idx = OLD_VERSIONS.iter().position(|&v| v == version_str).unwrap() + 1; 37 | } else { 38 | max_version -= 1; 39 | } 40 | 41 | let mut min_version = max_version - 30; 42 | 43 | if min_version < 4 { 44 | min_version = 4; 45 | } 46 | 47 | let mut out = String::new(); 48 | out.push_str(r#"
    "#); 49 | 50 | // For newer version 51 | for version in (min_version..=max_version).rev() { 52 | generate_release_link(&mut out, version, locale)?; 53 | } 54 | 55 | // For older version 56 | if min_version == 4 { 57 | for version in OLD_VERSIONS.iter().skip(old_version_start_idx) { 58 | generate_release_link(&mut out, version, locale)?; 59 | } 60 | } 61 | 62 | out.push_str("
"); 63 | Ok(out) 64 | } 65 | 66 | fn invalid_slug(slug: &str) -> DocError { 67 | DocError::InvalidSlugForX(format!("{slug}: firefox_for_developers templ")) 68 | } 69 | 70 | fn generate_release_link( 71 | out: &mut String, 72 | version: T, 73 | locale: Locale, 74 | ) -> Result<(), DocError> { 75 | let for_developers = l10n_json_data("Template", "for_developers", locale)?; 76 | out.push_str("
  • "); 77 | render_link_via_page( 78 | out, 79 | &format!("/Mozilla/Firefox/Releases/{version}"), 80 | locale, 81 | Some(&format!("Firefox {version} {for_developers}")), 82 | None, 83 | LinkFlags { 84 | code: false, 85 | with_badges: false, 86 | report: false, 87 | }, 88 | )?; 89 | out.push_str("
  • "); 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_list_alpha; 2 | pub mod api_list_specs; 3 | pub mod badges; 4 | pub mod banners; 5 | pub mod compat; 6 | pub mod css_ref; 7 | pub mod cssinfo; 8 | pub mod csssyntax; 9 | pub mod echo; 10 | pub mod embeds; 11 | pub mod firefox_for_developers; 12 | pub mod glossary; 13 | pub mod glossarydisambiguation; 14 | pub mod inheritance_diagram; 15 | pub mod inline_labels; 16 | pub mod js_property_attributes; 17 | pub mod links; 18 | pub mod list_subpages_for_sidebar; 19 | pub mod listsubpages; 20 | pub mod previous_menu_next; 21 | pub mod quick_links_with_subpages; 22 | pub mod sidebars; 23 | pub mod specification; 24 | pub mod subpages_with_summaries; 25 | pub mod svginfo; 26 | pub mod web_ext_examples; 27 | pub mod webext_all_examples; 28 | pub mod xsltref; 29 | 30 | use std::collections::HashMap; 31 | use std::sync::LazyLock; 32 | 33 | use rari_types::globals::deny_warnings; 34 | use rari_types::templ::{RariFn, TemplType}; 35 | use rari_types::{Arg, RariEnv}; 36 | use tracing::error; 37 | 38 | use crate::error::DocError; 39 | use crate::utils::TEMPL_RECORDER; 40 | 41 | #[derive(Debug)] 42 | pub struct Templ { 43 | pub name: &'static str, 44 | pub outline: &'static str, 45 | pub outline_snippet: &'static str, 46 | pub outline_plain: &'static str, 47 | pub doc: &'static str, 48 | pub function: RariFn>, 49 | pub typ: TemplType, 50 | } 51 | 52 | inventory::collect!(Templ); 53 | 54 | pub static TEMPL_MAP: LazyLock> = 55 | LazyLock::new(|| inventory::iter::().collect()); 56 | 57 | pub static TEMPL_MAPPING: LazyLock> = 58 | LazyLock::new(|| inventory::iter::().map(|t| (t.name, t)).collect()); 59 | 60 | pub fn exists(name: &str) -> bool { 61 | TEMPL_MAPPING.contains_key(name) 62 | } 63 | 64 | pub fn invoke( 65 | env: &RariEnv, 66 | name: &str, 67 | args: Vec>, 68 | ) -> Result<(String, TemplType), DocError> { 69 | let name = name.replace('-', "_"); 70 | let (f, is_sidebar) = match TEMPL_MAPPING.get(name.as_str()) { 71 | Some(t) => (t.function, t.typ), 72 | None if name == "xulelem" => return Ok((Default::default(), TemplType::None)), 73 | None if deny_warnings() => return Err(DocError::UnknownMacro(name.to_string())), 74 | None => { 75 | TEMPL_RECORDER.with(|tx| { 76 | if let Some(tx) = tx 77 | && let Err(e) = tx.send(name.to_string()) 78 | { 79 | error!("templ recorder: {e}"); 80 | } 81 | }); 82 | return Ok((format!("unsupported templ: {name}"), TemplType::None)); 83 | } // 84 | }; 85 | f(env, args).map(|s| (s, is_sidebar)) 86 | } 87 | 88 | #[cfg(test)] 89 | mod test { 90 | use super::*; 91 | 92 | #[test] 93 | fn test_kw() { 94 | println!("{:?}", *TEMPL_MAP); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Reusable Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | cancel-in-progress: 7 | description: "To cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true." 8 | type: boolean 9 | default: false 10 | prefix: 11 | description: "Deployment prefix" 12 | required: true 13 | type: string 14 | build-artifact-name: 15 | description: "Name of the build artifact to download" 16 | required: true 17 | type: string 18 | outputs: 19 | url: 20 | description: "Deployment URL" 21 | value: ${{ jobs.deploy.outputs.url }} 22 | 23 | permissions: 24 | contents: read 25 | # Authenticate with GCP. 26 | id-token: write 27 | 28 | concurrency: 29 | group: ci-${{ github.workflow }}-${{ inputs.prefix }} 30 | cancel-in-progress: ${{ inputs.cancel-in-progress }} 31 | 32 | env: 33 | BUILD_OUT_ROOT: ${{ github.workspace }}/mdn/fred/out 34 | PREFIX: ${{ inputs.prefix }} 35 | 36 | jobs: 37 | deploy: 38 | if: github.repository == 'mdn/rari' && github.secret_source == 'Actions' 39 | runs-on: ubuntu-latest 40 | 41 | outputs: 42 | url: ${{ steps.set-output.outputs.url }} 43 | 44 | environment: 45 | name: review 46 | url: https://${{ env.PREFIX }}.${{ vars.HOST }} 47 | 48 | steps: 49 | - name: Download build output 50 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 51 | with: 52 | name: ${{ inputs.build-artifact-name }} 53 | path: ${{ env.BUILD_OUT_ROOT }} 54 | 55 | - name: Authenticate with GCP 56 | uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 57 | with: 58 | token_format: access_token 59 | service_account: deploy-mdn-review-content@${{ secrets.GCP_PROJECT_NAME }}.iam.gserviceaccount.com 60 | workload_identity_provider: projects/${{ secrets.WIP_PROJECT_ID }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions 61 | 62 | - name: Setup gcloud 63 | uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 64 | 65 | - name: Upload to GCS 66 | uses: google-github-actions/upload-cloud-storage@6397bd7208e18d13ba2619ee21b9873edc94427a # v3.0.0 67 | with: 68 | path: ${{ env.BUILD_OUT_ROOT }} 69 | destination: "${{ vars.GCP_BUCKET_NAME }}/${{ env.PREFIX }}" 70 | resumable: false 71 | headers: |- 72 | cache-control: no-store 73 | parent: false 74 | concurrency: 500 75 | process_gcloudignore: false 76 | 77 | - name: Set deployment URL output 78 | id: set-output 79 | env: 80 | URL: "https://${{ env.PREFIX }}.${{ vars.HOST }}/" 81 | run: | 82 | echo "url=$URL" >> $GITHUB_OUTPUT 83 | -------------------------------------------------------------------------------- /crates/rari-doc/src/helpers/title.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DocError; 2 | use crate::pages::page::{Page, PageLike}; 3 | 4 | pub fn transform_title(title: &str) -> &str { 5 | match title { 6 | "Web technology for developers" => "References", 7 | "Learn web development" => "Learn", 8 | "HTML: HyperText Markup Language" => "HTML", 9 | "CSS: Cascading Style Sheets" => "CSS", 10 | "Graphics on the Web" => "Graphics", 11 | "HTML elements reference" => "Elements", 12 | "JavaScript reference" => "Reference", 13 | "JavaScript Guide" => "Guide", 14 | "Structuring the web with HTML" => "HTML", 15 | "Learn to style HTML using CSS" => "CSS", 16 | "Web forms — Working with user data" => "Forms", 17 | _ => title, 18 | } 19 | } 20 | 21 | pub fn page_title(doc: &impl PageLike, with_suffix: bool) -> Result { 22 | let mut out = String::new(); 23 | 24 | out.push_str(doc.title()); 25 | 26 | if let Some(root_url) = root_doc_url(doc.url()) 27 | && root_url != doc.url() 28 | { 29 | let root_doc = Page::from_url_with_fallback(root_url)?; 30 | out.push_str(" - "); 31 | out.push_str(root_doc.short_title().unwrap_or(root_doc.title())); 32 | } 33 | if with_suffix && let Some(suffix) = doc.title_suffix() { 34 | out.push_str(" | "); 35 | out.push_str(suffix); 36 | } 37 | Ok(out) 38 | } 39 | 40 | pub fn root_doc_url(url: &str) -> Option<&str> { 41 | let m = url 42 | .match_indices('/') 43 | .map(|(i, _)| i) 44 | .zip(url.split('/').skip(1)) 45 | .collect::>(); 46 | if m.len() < 3 { 47 | return None; 48 | } 49 | if matches!(m[1].1, "blog" | "curriculum") { 50 | return None; 51 | } 52 | if m[1].1 == "docs" { 53 | if m[2].1 == "Web" { 54 | if let Some(base) = m.iter().rfind(|p| matches!(p.1, "Guides" | "Reference")) { 55 | return Some(&url[..base.0]); 56 | } 57 | return Some(&url[..*m.get(4).map(|(i, _)| i).unwrap_or(&url.len())]); 58 | } 59 | if matches!(m[2].1, "conflicting" | "orphaned") { 60 | return None; 61 | } 62 | } 63 | Some(&url[..*m.get(3).map(|(i, _)| i).unwrap_or(&url.len())]) 64 | } 65 | 66 | #[cfg(test)] 67 | mod test { 68 | use super::*; 69 | 70 | #[test] 71 | fn test_root_doc_url() { 72 | assert_eq!( 73 | root_doc_url("/en-US/docs/Web/CSS/border"), 74 | Some("/en-US/docs/Web/CSS") 75 | ); 76 | assert_eq!( 77 | root_doc_url("/en-US/docs/Web/CSS"), 78 | Some("/en-US/docs/Web/CSS") 79 | ); 80 | assert_eq!( 81 | root_doc_url("/en-US/docs/Learn/foo"), 82 | Some("/en-US/docs/Learn") 83 | ); 84 | assert_eq!(root_doc_url("/en-US/blog/foo"), None); 85 | assert_eq!(root_doc_url("/en-US/curriculum/foo"), None); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/banners.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | use rari_utils::concat_strs; 4 | use tracing::warn; 5 | 6 | use crate::error::DocError; 7 | use crate::helpers::l10n::l10n_json_data; 8 | 9 | #[rari_f(register = "crate::Templ", typ = "TemplType::Banner")] 10 | pub fn deprecated_header(version: Option) -> Result { 11 | if version.is_some() { 12 | warn!("Do not use deprecated header with parameter!") 13 | } 14 | let title = l10n_json_data("Template", "deprecated_badge_abbreviation", env.locale)?; 15 | let copy = l10n_json_data("Template", "deprecated_header_copy", env.locale)?; 16 | 17 | Ok(concat_strs!( 18 | r#"

    "#, 19 | title, 20 | ": ", 21 | copy, 22 | "

    " 23 | )) 24 | } 25 | 26 | #[rari_f(register = "crate::Templ", typ = "TemplType::Banner")] 27 | pub fn availableinworkers(typ: Option) -> Result { 28 | let default_typ = "available_in_worker__default"; 29 | let typ = typ 30 | .map(|s| s.to_lowercase()) 31 | .map(|typ| format!("available_in_worker__{typ}")); 32 | let copy = l10n_json_data( 33 | "Template", 34 | typ.as_deref().unwrap_or(default_typ), 35 | env.locale, 36 | ) 37 | .unwrap_or(l10n_json_data("Template", default_typ, env.locale)?); 38 | 39 | Ok(concat_strs!( 40 | r#"

    "#, 41 | copy, 42 | "

    " 43 | )) 44 | } 45 | 46 | #[rari_f(register = "crate::Templ", typ = "TemplType::Banner")] 47 | pub fn seecompattable() -> Result { 48 | let title = l10n_json_data("Template", "experimental_badge_abbreviation", env.locale)?; 49 | let copy = l10n_json_data("Template", "see_compat_table_copy", env.locale)?; 50 | 51 | Ok(concat_strs!( 52 | r#"

    "#, 53 | title, 54 | ": ", 55 | copy, 56 | "

    " 57 | )) 58 | } 59 | 60 | #[rari_f(register = "crate::Templ", typ = "TemplType::Banner")] 61 | pub fn securecontext_header() -> Result { 62 | let title = l10n_json_data("Template", "secure_context_label", env.locale)?; 63 | let copy = l10n_json_data("Template", "secure_context_header_copy", env.locale)?; 64 | 65 | Ok(concat_strs!( 66 | r#"

    "#, 67 | &html_escape::encode_double_quoted_attribute(title), 68 | ": ", 69 | copy, 70 | "

    " 71 | )) 72 | } 73 | 74 | #[rari_f(register = "crate::Templ", typ = "TemplType::Banner")] 75 | pub fn non_standard_header() -> Result { 76 | let title = l10n_json_data("Template", "non_standard_badge_abbreviation", env.locale)?; 77 | let copy = l10n_json_data("Template", "non_standard_header_copy", env.locale)?; 78 | 79 | Ok(concat_strs!( 80 | r#"

    "#, 81 | title, 82 | ": ", 83 | copy, 84 | "

    " 85 | )) 86 | } 87 | -------------------------------------------------------------------------------- /crates/rari-deps/src/popularities.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::BufWriter; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use chrono::{DateTime, Datelike, Utc}; 6 | use rari_types::Popularities; 7 | use rari_utils::io::read_to_string; 8 | use serde::Deserialize; 9 | 10 | use crate::current::Current; 11 | use crate::error::DepsError; 12 | 13 | #[derive(Debug, Clone, Deserialize)] 14 | pub struct PopularityRow { 15 | #[serde(rename = "Page")] 16 | pub page: String, 17 | #[serde(rename = "Pageviews")] 18 | pub page_views: f64, 19 | } 20 | 21 | const CURRENT_URL: &str = "https://popularities.mdn.mozilla.net/current.csv"; 22 | const LIMIT: usize = 20_000; 23 | 24 | fn should_update(now: &DateTime, current: &Option>) -> bool { 25 | let now_date = now.date_naive(); 26 | if let Some(current) = current { 27 | let current_date = current.date_naive(); 28 | // True if it'a at least 2nd day of a new month vs. current. 29 | (current_date.year() < now_date.year() || current_date.month() < now_date.month()) 30 | && now_date.day() > 1 31 | } else { 32 | true 33 | } 34 | } 35 | 36 | pub fn update_popularities(base_path: &Path) -> Result, DepsError> { 37 | let package_path = base_path.join("popularities"); 38 | let last_check_path = package_path.join("last_check.json"); 39 | let now = Utc::now(); 40 | let current = read_to_string(last_check_path) 41 | .ok() 42 | .and_then(|current| serde_json::from_str::(¤t).ok()) 43 | .unwrap_or_default(); 44 | 45 | if should_update(&now, ¤t.latest_last_check) { 46 | let mut popularities = Popularities { 47 | popularities: Default::default(), 48 | date: Utc::now().naive_utc(), 49 | }; 50 | 51 | let mut max = f64::INFINITY; 52 | let pop_csv = reqwest::blocking::get(CURRENT_URL).expect("unable to download popularities"); 53 | let mut rdr = csv::Reader::from_reader(pop_csv); 54 | for row in rdr.deserialize::().flatten().take(LIMIT) { 55 | if row.page.contains("/docs/") && !row.page.contains(['$', '?']) { 56 | if max.is_infinite() { 57 | max = row.page_views; 58 | } 59 | popularities 60 | .popularities 61 | .insert(row.page, row.page_views / max); 62 | } 63 | } 64 | 65 | let artifact_path = package_path.join("popularities.json"); 66 | if package_path.exists() { 67 | fs::remove_dir_all(&package_path)?; 68 | } 69 | fs::create_dir_all(&package_path)?; 70 | 71 | let file = File::create(artifact_path).unwrap(); 72 | let buffed = BufWriter::new(file); 73 | 74 | serde_json::to_writer_pretty(buffed, &popularities).unwrap(); 75 | 76 | fs::write( 77 | package_path.join("last_check.json"), 78 | serde_json::to_string_pretty(&Current { 79 | current_version: None, 80 | latest_last_check: Some(now), 81 | })?, 82 | )?; 83 | return Ok(Some(package_path)); 84 | } 85 | Ok(None) 86 | } 87 | -------------------------------------------------------------------------------- /crates/rari-tools/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | 4 | use rari_doc::error::DocError; 5 | use rari_doc::pages::page::{Page, PageLike}; 6 | use rari_doc::pages::types::doc::Doc; 7 | use rari_doc::reader::read_docs_parallel; 8 | use rari_types::globals::{content_root, content_translated_root}; 9 | use rari_types::locale::Locale; 10 | use tracing::warn; 11 | 12 | use crate::error::ToolError; 13 | use crate::redirects::{self, redirects_path}; 14 | 15 | pub(crate) fn parent_slug(slug: &str) -> Result<&str, ToolError> { 16 | let slug = slug.trim_end_matches('/'); 17 | if let Some(i) = slug.rfind('/') { 18 | Ok(&slug[..i]) 19 | } else { 20 | Err(ToolError::InvalidSlug(Cow::Borrowed("slug has no parent"))) 21 | } 22 | } 23 | 24 | /// Read all en-US and translated documents into a hash, with a key of `(locale, slug)`. 25 | /// This is similar to the `cached_reader` functionality, but not wrapped in a `onceLock`. 26 | pub(crate) fn read_all_doc_pages() -> Result), Page>, DocError> { 27 | let docs = read_docs_parallel::(&[content_root()], None)?; 28 | let mut docs_hash: HashMap<(Locale, Cow<'_, str>), Page> = docs 29 | .iter() 30 | .cloned() 31 | .map(|doc| ((doc.locale(), Cow::Owned(doc.slug().to_string())), doc)) 32 | .collect(); 33 | 34 | if let Some(translated_root) = content_translated_root() { 35 | let translated_docs = read_docs_parallel::(&[translated_root], None)?; 36 | docs_hash.extend( 37 | translated_docs 38 | .iter() 39 | .cloned() 40 | .map(|doc| ((doc.locale(), Cow::Owned(doc.slug().to_string())), doc)), 41 | ) 42 | } 43 | Ok(docs_hash) 44 | } 45 | 46 | pub(crate) fn get_redirects_map(locale: Locale) -> HashMap { 47 | let redirects_path = redirects_path(locale).unwrap(); 48 | let mut redirects = HashMap::new(); 49 | match redirects::read_redirects_raw(&redirects_path) { 50 | Ok(raw_redirects) => redirects.extend(raw_redirects), 51 | Err(e) => warn!("Could not read redirects for {}: {}", locale, e), 52 | } 53 | redirects 54 | } 55 | 56 | #[cfg(test)] 57 | pub mod test_utils { 58 | use std::path::Path; 59 | 60 | pub(crate) fn check_file_existence( 61 | root: &Path, 62 | should_exist: &[&str], 63 | should_not_exist: &[&str], 64 | ) { 65 | use std::path::PathBuf; 66 | 67 | for relative_path in should_exist { 68 | let parts = relative_path.split('/').collect::>(); 69 | let mut path = PathBuf::from(root); 70 | for part in parts { 71 | path.push(part); 72 | } 73 | assert!(path.exists(), "{} should exist", path.display()); 74 | } 75 | 76 | for relative_path in should_not_exist { 77 | let parts = relative_path.split('/').collect::>(); 78 | let mut path = PathBuf::from(root); 79 | for part in parts { 80 | path.push(part); 81 | } 82 | assert!(!path.exists(), "{} should not exist", path.display()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/rari-tools/src/tests/fixtures/wikihistory.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | use chrono::{DateTime, SecondsFormat}; 6 | use fake::Fake; 7 | use fake::faker::chrono::en::DateTimeBetween; 8 | use fake::faker::internet::en::Username; 9 | use rari_doc::utils::root_for_locale; 10 | use rari_types::locale::Locale; 11 | use serde_json::Value; 12 | 13 | #[allow(dead_code)] 14 | pub(crate) struct WikihistoryFixtures { 15 | path: PathBuf, 16 | do_not_remove: bool, 17 | } 18 | 19 | impl WikihistoryFixtures { 20 | pub fn new(slugs: &Vec, locale: Locale) -> Self { 21 | Self::new_internal(slugs, locale, false) 22 | } 23 | #[allow(dead_code)] 24 | pub fn debug_new(slugs: &Vec, locale: Locale) -> Self { 25 | Self::new_internal(slugs, locale, true) 26 | } 27 | fn new_internal(slugs: &Vec, locale: Locale, do_not_remove: bool) -> Self { 28 | // create wiki history file for each slug in the vector, in the configured root directory for the locale 29 | let mut folder_path = PathBuf::new(); 30 | folder_path.push(root_for_locale(locale).unwrap()); 31 | folder_path.push(locale.as_folder_str()); 32 | fs::create_dir_all(&folder_path).unwrap(); 33 | folder_path.push("_wikihistory.json"); 34 | 35 | let mut entries: BTreeMap = BTreeMap::new(); 36 | for slug in slugs { 37 | let value: BTreeMap = BTreeMap::from([ 38 | ( 39 | "modified".to_string(), 40 | Value::String(random_date_rfc3339_string()), 41 | ), 42 | ("contributors".to_string(), Value::Array(random_names())), 43 | ]); 44 | let map: serde_json::Map = value.into_iter().collect(); 45 | entries.insert(slug.to_string(), Value::Object(map)); 46 | } 47 | 48 | let mut json_string = serde_json::to_string_pretty(&entries).unwrap(); 49 | json_string.push('\n'); 50 | fs::write(&folder_path, json_string).unwrap(); 51 | 52 | WikihistoryFixtures { 53 | path: folder_path, 54 | do_not_remove, 55 | } 56 | } 57 | } 58 | 59 | impl Drop for WikihistoryFixtures { 60 | fn drop(&mut self) { 61 | if self.do_not_remove { 62 | tracing::info!( 63 | "Leaving wikihistory fixture {} in place for debugging", 64 | self.path.display() 65 | ); 66 | return; 67 | } 68 | 69 | fs::remove_file(&self.path).unwrap(); 70 | } 71 | } 72 | 73 | fn random_names() -> Vec { 74 | let num_entries = rand::random::() % 10 + 1; 75 | let names: Vec = (0..num_entries) 76 | .map(|_| Value::String(Username().fake())) 77 | .collect(); 78 | names 79 | } 80 | 81 | fn random_date_rfc3339_string() -> String { 82 | DateTimeBetween( 83 | DateTime::parse_from_rfc3339("2015-01-01T00:00:00Z") 84 | .unwrap() 85 | .to_utc(), 86 | DateTime::parse_from_rfc3339("2020-12-31T23:59:59Z") 87 | .unwrap() 88 | .to_utc(), 89 | ) 90 | .fake::>() 91 | .to_rfc3339_opts(SecondsFormat::Secs, true) 92 | } 93 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/jsxref.rs: -------------------------------------------------------------------------------- 1 | use rari_templ_func::rari_f; 2 | use rari_types::AnyArg; 3 | 4 | use crate::error::DocError; 5 | use crate::templ::api::RariApi; 6 | 7 | /// Creates a link to a JavaScript reference page on MDN. 8 | /// 9 | /// This macro generates links to JavaScript language features including objects, 10 | /// methods, properties, statements, operators, and other JavaScript reference 11 | /// documentation. It intelligently routes to either the main JavaScript Reference 12 | /// or the Global Objects section based on the API name. 13 | /// 14 | /// # Arguments 15 | /// * `api_name` - The JavaScript feature name (object, method, property, etc.) 16 | /// * `display` - Optional custom display text for the link 17 | /// * `anchor` - Optional anchor/fragment to append to the URL 18 | /// * `no_code` - Optional flag to disable code formatting (default: false) 19 | /// 20 | /// # Examples 21 | /// * `{{JSxRef("Array")}}` -> links to Array global object 22 | /// * `{{JSxRef("Array.prototype.map")}}` -> links to Array map method 23 | /// * `{{JSxRef("Promise", "Promises")}}` -> custom display text 24 | /// * `{{JSxRef("if...else")}}` -> links to if...else statement 25 | /// * `{{JSxRef("typeof", "", "", true)}}` -> disables code formatting 26 | /// 27 | /// # Special handling 28 | /// - Removes `()` from method names for URL generation 29 | /// - Converts `.prototype.` notation to `/` for URL paths 30 | /// - Tries main JavaScript Reference first, then Global Objects 31 | /// - Handles special cases like `try...catch` statements 32 | /// - Falls back to URI component decoding if no page found 33 | /// - Formats links with `` tags unless `no_code` is true 34 | #[rari_f(register = "crate::Templ")] 35 | pub fn jsxref( 36 | api_name: String, 37 | display: Option, 38 | anchor: Option, 39 | no_code: Option, 40 | ) -> Result { 41 | let display = display.as_deref().filter(|s| !s.is_empty()); 42 | let global_objects = "Global_Objects"; 43 | let display = display.unwrap_or(api_name.as_str()); 44 | let mut url = format!("/{}/docs/Web/JavaScript/Reference/", &env.locale); 45 | let mut base_path = url.clone(); 46 | 47 | let mut slug = api_name.replace("()", "").replace(".prototype.", "."); 48 | if !slug.contains("/") && slug.contains('.') { 49 | // Handle try...catch case 50 | slug = slug.replace('.', "/"); 51 | } 52 | 53 | let page_url = format!("{url}{slug}"); 54 | let object_page_url = format!("{url}{global_objects}/{slug}"); 55 | 56 | let page = RariApi::get_page_nowarn(&page_url); 57 | let object_page = RariApi::get_page_nowarn(&object_page_url); 58 | if let Ok(_page) = page { 59 | url.push_str(&slug) 60 | } else if let Ok(_object_page) = object_page { 61 | base_path.extend([global_objects, "/"]); 62 | url.extend([global_objects, "/", &slug]); 63 | } else { 64 | url.push_str(&RariApi::decode_uri_component(&api_name)); 65 | } 66 | 67 | if let Some(anchor) = anchor { 68 | if !anchor.starts_with('#') { 69 | url.push('#'); 70 | } 71 | url.push_str(&anchor); 72 | } 73 | 74 | let code = !no_code.map(|nc| nc.as_bool()).unwrap_or_default(); 75 | RariApi::link(&url, Some(env.locale), Some(display), code, None, false) 76 | } 77 | -------------------------------------------------------------------------------- /crates/rari-doc/src/contributors.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rari_types::locale::Locale; 4 | use serde::Deserialize; 5 | 6 | #[derive(Clone, Debug, Default, Deserialize)] 7 | pub struct WikiHistoryEntry { 8 | #[serde(default)] 9 | pub contributors: Vec, 10 | } 11 | 12 | pub type WikiHistory = HashMap; 13 | pub type WikiHistories = HashMap; 14 | 15 | /// Generates a contributors text report summarizing commit history and original Wiki contributors. 16 | /// 17 | /// This function creates a formatted string containing a list of contributors for a given file. 18 | /// It includes: 19 | /// - A section linking to the GitHub commit history for the file. 20 | /// - An optional section listing original Wiki contributors if historical data is available. 21 | /// 22 | /// # Parameters 23 | /// 24 | /// - `wiki_history`: An optional reference to a `WikiHistoryEntry`, which contains historical 25 | /// contributor data from the Wiki. If `None`, the Wiki contributors section is omitted. 26 | /// - `github_file_url`: A string containing the URL of the file on GitHub. The URL is modified 27 | /// to point to the file's commit history. 28 | /// 29 | /// # Returns 30 | /// 31 | /// A `String` formatted as a contributors report. The report consists of: 32 | /// 1. A header: `# Contributors by commit history` 33 | /// 2. A link to the GitHub commit history, derived from `github_file_url`. 34 | /// 3. If `wiki_history` is provided, an additional section: 35 | /// - A header: `# Original Wiki contributors` 36 | /// - A newline-separated list of contributors from `wiki_history`. 37 | /// 38 | /// # Example 39 | /// 40 | /// ```rust 41 | /// # use rari_doc::contributors::contributors_txt; 42 | /// # use rari_doc::contributors::WikiHistoryEntry; 43 | /// 44 | /// let github_file_url = "https://github.com/user/repo/blob/main/file.txt"; 45 | /// let wiki_history = Some(WikiHistoryEntry { 46 | /// contributors: vec!["Alice".to_string(), "Bob".to_string()], 47 | /// }); 48 | /// let result = contributors_txt(wiki_history.as_ref(), github_file_url); 49 | /// println!("{}", result); 50 | /// // Output: 51 | /// // # Contributors by commit history 52 | /// // https://github.com/user/repo/commits/main/file.txt 53 | /// // 54 | /// // # Original Wiki contributors 55 | /// // Alice 56 | /// // Bob 57 | /// ``` 58 | /// 59 | /// If no `wiki_history` is provided: 60 | /// 61 | /// ```rust 62 | /// # use rari_doc::contributors::contributors_txt; 63 | /// let github_file_url = "https://github.com/user/repo/blob/main/file.txt"; 64 | /// let result = contributors_txt(None, github_file_url); 65 | /// println!("{}", result); 66 | /// // Output: 67 | /// // # Contributors by commit history 68 | /// // https://github.com/user/repo/commits/main/file.txt 69 | /// ``` 70 | pub fn contributors_txt(wiki_history: Option<&WikiHistoryEntry>, github_file_url: &str) -> String { 71 | let mut out = String::new(); 72 | out.extend([ 73 | "# Contributors by commit history\n", 74 | &github_file_url.replace("blob", "commits"), 75 | "\n\n", 76 | ]); 77 | if let Some(wh) = wiki_history 78 | && !wh.contributors.is_empty() 79 | { 80 | out.extend([ 81 | "# Original Wiki contributors\n", 82 | &wh.contributors.join("\n"), 83 | "\n", 84 | ]); 85 | } 86 | out 87 | } 88 | -------------------------------------------------------------------------------- /crates/rari-doc/src/templ/templs/links/domxref.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rari_templ_func::rari_f; 4 | use rari_types::{AnyArg, ArgError}; 5 | use tracing::{Level, span}; 6 | 7 | use crate::error::DocError; 8 | use crate::templ::api::RariApi; 9 | 10 | /// Creates a link to a DOM/Web API reference page on MDN. 11 | /// 12 | /// This macro generates links to Web API interfaces, methods, properties, and other 13 | /// DOM-related documentation. It handles various API naming conventions and can 14 | /// automatically format display text and anchors for methods and properties. 15 | /// 16 | /// # Arguments 17 | /// * `api_name` - The API name (interface, method, property, etc.) 18 | /// * `display` - Optional custom display text for the link 19 | /// * `anchor` - Optional anchor/fragment to append to the URL 20 | /// * `no_code` - Optional flag to disable code formatting (default: false) 21 | /// 22 | /// # Examples 23 | /// * `{{DOMxRef("Document")}}` -> links to Document interface 24 | /// * `{{DOMxRef("document.getElementById()")}}` -> links to getElementById method 25 | /// * `{{DOMxRef("Element.innerHTML", "innerHTML property")}}` -> custom display text 26 | /// * `{{DOMxRef("Node", "", "", true)}}` -> disables code formatting 27 | /// 28 | /// # Special handling 29 | /// - Converts spaces to underscores and removes `()` from method names 30 | /// - Handles prototype chain notation (`.prototype.` becomes `/`) 31 | /// - Automatically capitalizes first letter of interface names in URLs 32 | /// - Appends method/property names to display text when using anchors 33 | /// - Formats links with `` tags unless `no_code` is true 34 | #[rari_f(register = "crate::Templ")] 35 | pub fn domxref( 36 | api_name: String, 37 | display: Option, 38 | anchor: Option, 39 | no_code: Option, 40 | ) -> Result { 41 | let span = span!(Level::ERROR, "domxref", basepath = "/docs/Web/API/"); 42 | let _enter = span.enter(); 43 | let display = display.as_deref().filter(|s| !s.is_empty()); 44 | let mut display_with_fallback = Cow::Borrowed(display.unwrap_or(api_name.as_str())); 45 | let api = api_name 46 | .replace(' ', "_") 47 | .replace("()", "") 48 | .replace(".prototype.", ".") 49 | .replace('.', "/"); 50 | if api.is_empty() { 51 | return Err(DocError::ArgError(ArgError::MustBeProvided)); 52 | } 53 | let (first_char_index, _) = api.char_indices().next().unwrap_or_default(); 54 | let mut url = format!( 55 | "/{}/docs/Web/API/{}{}", 56 | env.locale.as_url_str(), 57 | &api[0..first_char_index].to_uppercase(), 58 | &api[first_char_index..], 59 | ); 60 | if let Some(anchor) = anchor { 61 | if !anchor.starts_with('#') { 62 | url.push('#'); 63 | display_with_fallback = Cow::Owned(format!("{display_with_fallback}.{anchor}")); 64 | } 65 | url.push_str(&anchor); 66 | if let Some(anchor) = anchor.strip_prefix('#') { 67 | display_with_fallback = Cow::Owned(format!("{display_with_fallback}.{anchor}")); 68 | } 69 | } 70 | 71 | let code = !no_code.map(|nc| nc.as_bool()).unwrap_or_default(); 72 | RariApi::link( 73 | &url, 74 | Some(env.locale), 75 | Some(&display_with_fallback), 76 | code, 77 | display, 78 | false, 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/build-content-and-diff.yml: -------------------------------------------------------------------------------- 1 | name: Build Content and Diff 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**/*.rs' 7 | - '**/Cargo.lock' 8 | - '**/Cargo.toml' 9 | - '.github/workflows/build-content-and-diff.yml' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 13 | cancel-in-progress: true 14 | 15 | permissions: {} # No GITHUB_TOKEN permissions, as we only use it to increase API limit. 16 | 17 | env: 18 | BUILD_BASE: ${{ github.workspace }}/build-base 19 | BUILD_PR: ${{ github.workspace }}/build-pr 20 | CONTENT_ROOT: ${{ github.workspace }}/content/files 21 | DIFF: ${{ github.workspace }}/diff.html 22 | CARGO_TERM_COLOR: always 23 | 24 | jobs: 25 | build-and-diff: 26 | runs-on: ubuntu-latest 27 | env: 28 | SCCACHE_GHA_ENABLED: "true" 29 | RUSTC_WRAPPER: "sccache" 30 | 31 | steps: 32 | - name: Checkout (content) 33 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 34 | with: 35 | repository: mdn/content 36 | path: content 37 | persist-credentials: false 38 | 39 | - name: Setup Rust 40 | uses: dtolnay/rust-toolchain@9a1d20035bdbcbc899baabe1e402e85bc33639bc # 1.90 41 | 42 | - name: Setup sccache 43 | uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 44 | 45 | - name: Checkout (rari @ base) 46 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 47 | with: 48 | ref: ${{ github.base_ref }} 49 | path: rari-base 50 | persist-credentials: false 51 | 52 | - name: Checkout previous release (release-please PR) 53 | if: startsWith(github.head_ref, 'release-please--') 54 | working-directory: rari-base 55 | run: | 56 | PREV_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version') 57 | if [[ -n "$PREV_VERSION" ]]; then 58 | echo "Building release-please PR against previous release: v$PREV_VERSION" 59 | git fetch origin tag "v$PREV_VERSION" --no-tags 60 | git checkout "v$PREV_VERSION" 61 | fi 62 | 63 | - name: Checkout (rari @ PR) 64 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 65 | with: 66 | path: rari-pr 67 | persist-credentials: false 68 | 69 | - name: Build with base 70 | working-directory: rari-base 71 | env: 72 | BUILD_OUT_ROOT: ${{ env.BUILD_BASE }} 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: cargo run --release build 75 | 76 | - name: Build with PR 77 | working-directory: rari-pr 78 | env: 79 | BUILD_OUT_ROOT: ${{ env.BUILD_PR }} 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: cargo run --release build 82 | 83 | - name: Generate diff 84 | working-directory: rari-pr 85 | run: cargo run -p diff-test diff --fast --html --value --out "$DIFF" --verbose "$BUILD_BASE" "$BUILD_PR" 86 | 87 | - name: Upload diff 88 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 89 | with: 90 | name: build-diff-report 91 | path: ${{ env.DIFF }} 92 | if-no-files-found: error 93 | --------------------------------------------------------------------------------