| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| pub mod html; |
| pub mod markdown; |
| |
| use anyhow::Error; |
| use handlebars::{Context, Handlebars, Helper, JsonRender, Output, RenderContext, RenderError}; |
| use lazy_static::lazy_static; |
| use log::debug; |
| use pulldown_cmark::{html as pulldown_html, Parser}; |
| use regex::{Captures, Regex}; |
| use serde_json::Value; |
| |
| pub trait FidldocTemplate { |
| fn render_main_page(&self, main_fidl_json: &Value) -> Result<(), Error>; |
| fn render_interface(&self, package: &str, fidl_json: &Value) -> Result<(), Error>; |
| fn name(&self) -> String; |
| } |
| |
| pub type HandlebarsHelper = fn( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError>; |
| |
| pub fn get_link_helper( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for get link helper"))?; |
| debug!("get_link_helper called on {}", param.value().render()); |
| out.write(&sanitize_name(¶m.value().render()))?; |
| Ok(()) |
| } |
| |
| fn sanitize_name(name: &str) -> String { |
| name.replace("/", "_") |
| } |
| |
| pub fn remove_package_name( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param = h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for rpn helper"))?; |
| debug!("remove_package_name called on {}", param.value().render()); |
| out.write(&rpn(¶m.value().render()))?; |
| Ok(()) |
| } |
| |
| fn rpn(name: &str) -> String { |
| lazy_static! { |
| static ref RE: Regex = Regex::new(r".*/").unwrap(); |
| } |
| RE.replace_all(&name, "").into_owned() |
| } |
| |
| pub fn eq( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param_a = h |
| .param(0) |
| .ok_or_else(|| RenderError::new("Param 0 is required for eq helper"))? |
| .value() |
| .to_string(); |
| let param_b = h |
| .param(1) |
| .ok_or_else(|| RenderError::new("Param 1 is required for eq helper"))? |
| .value() |
| .to_string(); |
| debug!("eq called on {} and {}", param_a, param_b); |
| if param_a == param_b { |
| out.write("true")?; |
| } |
| Ok(()) |
| } |
| |
| fn package_link( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let package = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for pl helper"))?; |
| let base = h.param(1).ok_or_else(|| RenderError::new("Param 1 is required for pl helper"))?; |
| debug!("package_link called on {} and {}", package.value().render(), base.value().render()); |
| out.write(&pl(&package.value().render(), &base.value().render()))?; |
| Ok(()) |
| } |
| |
| fn pl(name: &str, base: &str) -> String { |
| let package_base; |
| let package_name; |
| |
| lazy_static! { |
| static ref RE: Regex = Regex::new(r"(.*)/(.*)").unwrap(); |
| } |
| |
| if RE.is_match(&name) { |
| // e.g. "fuchsia.media/Audio" |
| let caps = RE.captures(&name).expect("Expecting base/package"); |
| package_base = caps.get(1).unwrap().as_str(); |
| package_name = caps.get(2).unwrap().as_str(); |
| } else { |
| // e.g. "Audio" |
| package_base = &base; |
| package_name = &name; |
| } |
| |
| if package_base == base { |
| format!("<a class='link' href='#{anchor}'>{anchor}</a>", anchor = package_name) |
| } else { |
| format!("<a class='link' href='../{b}/'>{b}</a>/<a class='link' href='../{b}/#{anchor}'>{anchor}</a>", b = package_base, anchor = package_name) |
| } |
| } |
| |
| fn doc_link( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let docs = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for doc_link helper"))?; |
| let base = |
| h.param(1).ok_or_else(|| RenderError::new("Param 1 is required for doc_link helper"))?; |
| let docstring = docs.value().render(); |
| debug!("doc_link called on {} and {}", docstring, base.value().render()); |
| out.write(&dl(&docstring, &base.value().render()))?; |
| Ok(()) |
| } |
| |
| fn dl(docstring: &str, base: &str) -> String { |
| lazy_static! { |
| static ref RE: Regex = Regex::new(r"\[`(.*?)`\]").unwrap(); |
| } |
| RE.replace_all(&docstring, |caps: &Captures<'_>| { |
| let package = caps.get(1).unwrap().as_str(); |
| debug!("dl captured {}", package); |
| pl(package, base) |
| }) |
| .to_string() |
| } |
| |
| pub fn remove_parent_folders( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param = h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for rpf helper"))?; |
| debug!("remove_parent_folders called on {}", param.value().render()); |
| out.write(&rpf(¶m.value().render()))?; |
| Ok(()) |
| } |
| |
| fn rpf(path: &str) -> String { |
| lazy_static! { |
| static ref RE: Regex = Regex::new(r"^(\.\./)*").unwrap(); |
| } |
| RE.replace_all(&path, "").into_owned() |
| } |
| |
| fn source_link( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| let config = h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for sl helper"))?; |
| let tag = h.param(1).ok_or_else(|| RenderError::new("Param 1 is required for sl helper"))?; |
| let location = |
| h.param(2).ok_or_else(|| RenderError::new("Param 2 is required for sl helper"))?; |
| debug!( |
| "source_link called on {}, {} and {}", |
| config.value().to_string(), |
| tag.value().to_string(), |
| location.value().to_string() |
| ); |
| out.write(&sl(&config.value(), &tag.value(), &location.value()))?; |
| Ok(()) |
| } |
| |
| fn sl(config: &Value, tag: &Value, location: &Value) -> String { |
| if location.get("line").is_some() { |
| // Output source link with line number |
| format!( |
| "{baseUrl}{tag}{filenamePrefix}{filename}{linePrefix}{lineNo}", |
| baseUrl = config["source"]["baseUrl"].as_str().unwrap(), |
| tag = tag.as_str().unwrap(), |
| filenamePrefix = config["source"]["filenamePrefix"].as_str().unwrap(), |
| filename = rpf(location["filename"].as_str().unwrap()), |
| linePrefix = config["source"]["line"].as_str().unwrap(), |
| lineNo = location["line"] |
| ) |
| } else { |
| // Output general source link, without line number |
| format!( |
| "{baseUrl}{tag}{filenamePrefix}{filename}", |
| baseUrl = config["source"]["baseUrl"].as_str().unwrap(), |
| tag = tag.as_str().unwrap(), |
| filenamePrefix = config["source"]["filenamePrefix"].as_str().unwrap(), |
| filename = rpf(location["filename"].as_str().unwrap()) |
| ) |
| } |
| } |
| |
| pub fn one_line( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for oneline helper"))?; |
| debug!("oneline called on {}", param.value().render()); |
| out.write(&ol(¶m.value().render()))?; |
| Ok(()) |
| } |
| |
| fn ol(description: &str) -> String { |
| let lines: Vec<&str> = description.split("\n\n").collect(); |
| // If no match for \n\n, lines will contain the description as is. |
| lines.first().unwrap().to_string() |
| } |
| |
| pub fn pulldown( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| // get parameter from helper or throw an error |
| let param = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for pulldown helper"))?; |
| debug!("pulldown called on {}", param.value().render()); |
| out.write(&pd(¶m.value().render()))?; |
| Ok(()) |
| } |
| |
| fn pd(text: &str) -> String { |
| let parser = Parser::new(text); |
| let mut html_text = String::new(); |
| pulldown_html::push_html(&mut html_text, parser); |
| html_text |
| } |
| |
| pub fn method_id( |
| h: &Helper<'_, '_>, |
| _: &Handlebars<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| let method_name = |
| h.param(0).ok_or_else(|| RenderError::new("Param 0 is required for method_id helper"))?; |
| let protocol_name = |
| h.param(1).ok_or_else(|| RenderError::new("Param 1 is required for method_id helper"))?; |
| debug!( |
| "method_id called on {} and {}", |
| method_name.value().to_string(), |
| protocol_name.value().to_string() |
| ); |
| out.write(&mi(&method_name.value().render(), &protocol_name.value().render()))?; |
| Ok(()) |
| } |
| |
| fn mi(method: &str, protocol: &str) -> String { |
| format!("{protocol}.{method}", protocol = protocol, method = method) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use serde_json::json; |
| |
| #[test] |
| fn sanitize_name_test() { |
| let name = "base/name"; |
| assert_eq!(sanitize_name(name), "base_name"); |
| } |
| |
| #[test] |
| fn rpn_test() { |
| let name = "many/nested/levels/before/name"; |
| assert_eq!(rpn(name), "name"); |
| } |
| |
| #[test] |
| fn pl_test() { |
| let name = "fuchsia.media/Audio"; |
| let base = "fuchsia.media"; |
| let expected = "<a class='link' href='#Audio'>Audio</a>"; |
| assert_eq!(pl(name, base), expected.to_string()); |
| |
| let name = "fuchsia.media/Metadata"; |
| let base = "fuchsia.media.sessions"; |
| let expected = "<a class='link' href='../fuchsia.media/'>fuchsia.media</a>/<a class='link' href='../fuchsia.media/#Metadata'>Metadata</a>"; |
| assert_eq!(pl(name, base), expected.to_string()); |
| |
| let name = "Audio"; |
| let base = "fuchsia.media"; |
| let expected = "<a class='link' href='#Audio'>Audio</a>"; |
| assert_eq!(pl(name, base), expected.to_string()); |
| } |
| |
| #[test] |
| fn dl_test() { |
| let docs = "This is the the documentation for [`fuchsia.media/Audio`]"; |
| let base = "fuchsia.media"; |
| let expected = "This is the the documentation for <a class='link' href='#Audio'>Audio</a>"; |
| assert_eq!(dl(docs, base), expected.to_string()); |
| |
| let docs = "This is [`fuchsia.media/Audio`] and this is [`fuchsia.media.sessions/Publisher`], while this is not a link to fuchsia.io/Node."; |
| let base = "fuchsia.media.sessions"; |
| let expected = "This is <a class='link' href='../fuchsia.media/'>fuchsia.media</a>/<a class='link' href='../fuchsia.media/#Audio'>Audio</a> and this is <a class='link' href='#Publisher'>Publisher</a>, while this is not a link to fuchsia.io/Node."; |
| assert_eq!(dl(docs, base), expected.to_string()); |
| } |
| |
| #[test] |
| fn rpf_test() { |
| let path = "../../sdk/fidl/fuchsia-web/frame.fidl"; |
| assert_eq!(rpf(path), "sdk/fidl/fuchsia-web/frame.fidl"); |
| |
| let path = "sdk/fidl/fuchsia-web/frame.fidl"; |
| assert_eq!(rpf(path), "sdk/fidl/fuchsia-web/frame.fidl"); |
| |
| let path = "../../sdk/fidl/../fuchsia-web/frame.fidl"; |
| assert_eq!(rpf(path), "sdk/fidl/../fuchsia-web/frame.fidl"); |
| } |
| |
| #[test] |
| fn sl_test() { |
| let fidl_json = json!({ |
| "tag": "HEAD", |
| "config": json!({ |
| "source": json!({ |
| "baseUrl": "https://example.com/", |
| "filenamePrefix": ":", |
| "line": "#" |
| }) |
| }) |
| }); |
| |
| let location_line = json!({ |
| "filename": "sample.fidl", |
| "line": 42 |
| }); |
| |
| assert_eq!( |
| sl(&fidl_json["config"], &fidl_json["tag"], &location_line), |
| "https://example.com/HEAD:sample.fidl#42" |
| ); |
| |
| let location_no_line = json!({ |
| "filename": "foobar.fidl" |
| }); |
| |
| assert_eq!( |
| sl(&fidl_json["config"], &fidl_json["tag"], &location_no_line), |
| "https://example.com/HEAD:foobar.fidl" |
| ); |
| |
| let location_with_folders = json!({ |
| "filename": "../../sdk/fidl/fuchsia.bluetooth/address.fidl" |
| }); |
| |
| assert_eq!( |
| sl(&fidl_json["config"], &fidl_json["tag"], &location_with_folders), |
| "https://example.com/HEAD:sdk/fidl/fuchsia.bluetooth/address.fidl" |
| ); |
| |
| let location_with_folders_and_line = json!({ |
| "filename": "../../sdk/fidl/fuchsia.bluetooth/address.fidl", |
| "line": 9 |
| }); |
| |
| assert_eq!( |
| sl(&fidl_json["config"], &fidl_json["tag"], &location_with_folders_and_line), |
| "https://example.com/HEAD:sdk/fidl/fuchsia.bluetooth/address.fidl#9" |
| ); |
| } |
| |
| #[test] |
| fn ol_test() { |
| let description = "some text\nmore text\n\nto be ignored\n"; |
| assert_eq!(ol(description), "some text\nmore text"); |
| |
| let no_newlines = "some text"; |
| assert_eq!(ol(no_newlines), "some text"); |
| |
| let multiple_newlines = "some text\n\nmore text\n\neven more text"; |
| assert_eq!(ol(multiple_newlines), "some text"); |
| } |
| |
| #[test] |
| fn pd_test() { |
| let description = r#"In addition, clients may assert the type of the object by setting |
| the protocol corresponding to the expected type: |
| * If the caller expected a directory but the node cannot be accessed |
| as a directory, the error is `ZX_ERR_NOT_DIR`. |
| * If the caller expected a file but the node cannot be accessed as a |
| file, the error is `ZX_ERR_NOT_FILE`. |
| * In other mismatched cases, the error is `ZX_ERR_WRONG_TYPE`. |
| "#; |
| |
| let expected = r#"<p>In addition, clients may assert the type of the object by setting |
| the protocol corresponding to the expected type:</p> |
| <ul> |
| <li>If the caller expected a directory but the node cannot be accessed |
| as a directory, the error is <code>ZX_ERR_NOT_DIR</code>.</li> |
| <li>If the caller expected a file but the node cannot be accessed as a |
| file, the error is <code>ZX_ERR_NOT_FILE</code>.</li> |
| <li>In other mismatched cases, the error is <code>ZX_ERR_WRONG_TYPE</code>.</li> |
| </ul> |
| "#; |
| assert_eq!(pd(description), expected); |
| } |
| |
| #[test] |
| fn mi_test() { |
| let method = "Clone"; |
| let protocol = "Directory"; |
| let expected = "Directory.Clone"; |
| assert_eq!(mi(method, protocol), expected.to_string()); |
| } |
| } |