diff --git a/lib/frontend/Html_client.ml b/lib/frontend/Html_client.ml
index 6d4a8e7..c1f55e8 100644
--- a/lib/frontend/Html_client.ml
+++ b/lib/frontend/Html_client.ml
@@ -17,9 +17,13 @@ open struct
end
module Xmlns = Xmlns_effect.Make ()
-module Loop_detection = Loop_detection_effect.Make ()
-type env = {forest: State.t; scope: URI.t option; section_depth: int}
+type env = {
+ forest: State.t;
+ scope: URI.t option;
+ section_depth: int;
+ loops: Loop_detection.t;
+}
let hx ~env attrs children =
P.std_tag (Format.sprintf "h%i" @@ min 6 env.section_depth) attrs children
@@ -120,25 +124,47 @@ and render_section ~env (section : T.content T.section) : P.node list =
| Some title ->
P.HTML.header [] [hx ~env [] @@ render_content ~env title]
end;
- (if Loop_detection.have_seen_uri_opt section.frontmatter.uri then
- P.txt "Transclusion loop detected, rendering stopped."
- else
- let@ () = Loop_detection.add_seen_uri_opt section.frontmatter.uri in
- P.HTML.null @@ render_content ~env section.mainmatter);
+ begin if
+ Loop_detection.have_seen_uri_opt section.frontmatter.uri env.loops
+ then P.txt "Transclusion loop detected, rendering stopped."
+ else
+ P.HTML.null
+ @@ render_content
+ ~env:
+ {
+ env with
+ loops =
+ Loop_detection.add_seen_uri_opt section.frontmatter.uri
+ env.loops;
+ }
+ section.mainmatter
+ end;
];
]
let render_article_as_div ?(heading_level = 0) (forest : State.t)
(article : T.content T.article) : P.node =
let env =
- {forest; section_depth = heading_level; scope = article.frontmatter.uri}
+ {
+ forest;
+ section_depth = heading_level;
+ scope = article.frontmatter.uri;
+ loops = Loop_detection.empty;
+ }
in
- let@ () = Loop_detection.run in
let reserved = [{prefix = ""; xmlns = "http://www.w3.org/1999/xhtml"}] in
let@ () = Xmlns.run ~reserved in
P.HTML.div
(List.map render_xmlns_prefix reserved)
[
- (let@ () = Loop_detection.add_seen_uri_opt article.frontmatter.uri in
- P.HTML.null @@ render_content ~env article.mainmatter);
+ P.HTML.null
+ @@ render_content
+ ~env:
+ {
+ env with
+ loops =
+ Loop_detection.add_seen_uri_opt article.frontmatter.uri
+ env.loops;
+ }
+ article.mainmatter;
]
diff --git a/lib/frontend/Legacy_xml_client.ml b/lib/frontend/Legacy_xml_client.ml
index 127c629..86f6809 100644
--- a/lib/frontend/Legacy_xml_client.ml
+++ b/lib/frontend/Legacy_xml_client.ml
@@ -23,10 +23,6 @@ module Xmlns = struct
run ~reserved:X.reserved_xmlnss @@ fun () -> k X.reserved_xmlnss
end
-module In_backmatter = Algaeff.Reader.Make (struct
- type t = bool
-end)
-
let local_path_components (config : Config.t) (uri : URI.t) =
let host = Option.get @@ URI.host uri in
let base_host = Option.get @@ URI.host config.url in
@@ -40,49 +36,41 @@ let local_base_url_string (config : Config.t) =
let route (forest : State.t) uri : URI.t =
match forest.={uri} with
| None -> uri
- | Some tree -> (
+ | Some tree -> begin
match Tree.to_evaluated tree with
| Some evaluated when evaluated.route_locally ->
let path = "" :: local_path_components forest.config uri in
URI.make ~path ()
- | _ -> uri)
-
-module Scope = struct
- open struct
- module E = Algaeff.Reader.Make (struct
- type t = URI.t option
- end)
+ | _ -> uri
end
- let read = E.read
-
- let run ~(forest : State.t) ~env kont =
- let@ () = E.run ~env in
- let loc_opt =
- let@ uri = Option.bind env in
- let@ path = Option.map @~ State.source_path_of_uri uri forest in
- let position =
- Range.{source = `File path; offset = 0; start_of_line = 0; line_num = 0}
- in
- Range.make (position, position)
- in
- let@ () = Reporter.with_loc loc_opt in
- kont ()
-end
-
-module Loop_detection = Loop_detection_effect.Make ()
-
let mainmatter_cache = Hashtbl.create 1000
+type env = {
+ forest: State.t;
+ in_backmatter: bool;
+ uri: URI.t option;
+ loops: Loop_detection.t;
+}
+
+let range ~env =
+ let@ uri = Option.bind env.uri in
+ let@ path = Option.map @~ State.source_path_of_uri uri env.forest in
+ let position =
+ Range.{source = `File path; offset = 0; start_of_line = 0; line_num = 0}
+ in
+ Range.make (position, position)
+
let render_xml_qname qname =
let qname = Xmlns.normalise_qname qname in
match qname.prefix with
| "" -> qname.uname
| _ -> Format.sprintf "%s:%s" qname.prefix qname.uname
-let render_xml_attr (forest : State.t) T.{key; value} =
+let render_xml_attr ~env T.{key; value} =
let str_value =
- Plain_text_client.string_of_content ~forest ~router:(route forest) value
+ Plain_text_client.string_of_content ~forest:env.forest
+ ~router:(route env.forest) value
in
P.string_attr (render_xml_qname key) "%s" str_value
@@ -100,113 +88,123 @@ let render_section_flags (dict : T.section_flags) =
X.optional_ X.numbered dict.numbered;
]
-let rec render_section forest (section : T.content T.section) : P.node =
+let rec render_section ~env (section : T.content T.section) : P.node =
let@ _ = Xmlns.run in
X.tree
(render_section_flags section.flags)
[
- render_frontmatter forest section.frontmatter;
- (let@ () = Scope.run ~forest ~env:section.frontmatter.uri in
- X.mainmatter []
- @@
- if Loop_detection.have_seen_uri_opt section.frontmatter.uri then
- [X.info [] [P.txt "Transclusion loop detected, rendering stopped."]]
- else
- let@ () = Loop_detection.add_seen_uri_opt section.frontmatter.uri in
- render_mainmatter forest section);
+ render_frontmatter ~env section.frontmatter;
+ begin
+ let env = {env with uri = section.frontmatter.uri} in
+ X.mainmatter []
+ @@
+ if Loop_detection.have_seen_uri_opt section.frontmatter.uri env.loops
+ then
+ [X.info [] [P.txt "Transclusion loop detected, rendering stopped."]]
+ else
+ render_mainmatter
+ ~env:
+ {
+ env with
+ loops =
+ Loop_detection.add_seen_uri_opt section.frontmatter.uri
+ env.loops;
+ }
+ section
+ end;
]
-and render_mainmatter forest (section : T.content T.section) =
+and render_mainmatter ~env (section : T.content T.section) =
match section.frontmatter.uri with
- | None -> render_content forest section.mainmatter
- | Some uri -> (
+ | None -> render_content ~env section.mainmatter
+ | Some uri -> begin
match Hashtbl.find_opt mainmatter_cache uri with
| None ->
- let nodes = render_content forest section.mainmatter in
+ let nodes = render_content ~env section.mainmatter in
Hashtbl.add mainmatter_cache uri nodes;
nodes
- | Some nodes -> nodes)
+ | Some nodes -> nodes
+ end
-and render_frontmatter (forest : State.t)
- (frontmatter : T.content T.frontmatter) : P.node =
+and render_frontmatter ~env (frontmatter : T.content T.frontmatter) : P.node =
let result =
X.frontmatter []
[
- render_attributions forest frontmatter.uri frontmatter.attributions;
- render_dates forest frontmatter.dates;
- X.conditional forest.dev
- (X.optional (X.source_path [] "%s") frontmatter.source_path);
+ render_attributions ~env frontmatter.uri frontmatter.attributions;
+ render_dates ~env frontmatter.dates;
+ X.conditional env.forest.dev
+ @@ X.optional (X.source_path [] "%s") frontmatter.source_path;
X.optional
(fun uri -> X.uri [] "%s" @@ URI.to_string uri)
frontmatter.uri;
X.optional
(fun uri ->
X.display_uri [] "%s"
- @@ URI.display_path_string ~base:forest.config.url uri)
+ @@ URI.display_path_string ~base:env.forest.config.url uri)
frontmatter.uri;
X.optional (X.route [] "%s")
- @@ Option.map (Fun.compose URI.to_string (route forest)) frontmatter.uri;
+ @@ Option.map
+ (Fun.compose URI.to_string (route env.forest))
+ frontmatter.uri;
begin match frontmatter.title with
| None -> X.null []
| Some _ ->
let title =
- State.get_expanded_title ?scope:(Scope.read ()) frontmatter forest
+ State.get_expanded_title ?scope:env.uri frontmatter env.forest
in
X.title
[
X.text_ "%s"
- @@ Plain_text_client.string_of_content ~forest
- ~router:(route forest) title;
+ @@ Plain_text_client.string_of_content ~forest:env.forest
+ ~router:(route env.forest) title;
]
- @@ render_content forest title
+ @@ render_content ~env title
end;
begin match frontmatter.taxon with
| None -> X.null []
- | Some taxon -> X.taxon [] @@ render_content forest taxon
+ | Some taxon -> X.taxon [] @@ render_content ~env taxon
end;
- X.null @@ List.map (render_meta forest) frontmatter.metas;
+ X.null @@ List.map (render_meta ~env) frontmatter.metas;
]
in
result
-and render_meta forest (key, body) =
- X.meta [X.name "%s" key] @@ render_content forest body
+and render_meta ~env (key, body) =
+ X.meta [X.name "%s" key] @@ render_content ~env body
-and render_content (forest : State.t) (Content content : T.content) :
- P.node list =
+and render_content ~env (Content content : T.content) : P.node list =
match content with
| T.Text txt0 :: T.Text txt1 :: content ->
- render_content forest (Content (T.Text (txt0 ^ txt1) :: content))
+ render_content ~env (Content (T.Text (txt0 ^ txt1) :: content))
| node :: content ->
- let xs = render_content_node forest node in
- let ys = render_content forest (Content content) in
+ let xs = render_content_node ~env node in
+ let ys = render_content ~env (Content content) in
xs @ ys
| [] -> []
-and render_content_node (forest : State.t) (node : 'a T.content_node) :
- P.node list =
+and render_content_node ~env (node : 'a T.content_node) : P.node list =
match node with
| Text str -> [P.txt "%s" str]
| CDATA str -> [P.txt ~raw:true "" str]
| Uri uri ->
- [P.txt "%s" (URI.display_path_string ~base:forest.config.url uri)]
- | Route_of_uri uri -> [P.txt "%s" (URI.to_string (route forest uri))]
+ [P.txt "%s" (URI.display_path_string ~base:env.forest.config.url uri)]
+ | Route_of_uri uri -> [P.txt "%s" (URI.to_string (route env.forest uri))]
| Xml_elt elt ->
let prefixes_to_add, (name, attrs, content) =
let@ () = Xmlns.within_scope in
( render_xml_qname elt.name,
- List.map (render_xml_attr forest) elt.attrs,
- render_content forest elt.content )
+ List.map (render_xml_attr ~env) elt.attrs,
+ render_content ~env elt.content )
in
let attrs =
let xmlns_attrs = List.map render_xmlns_prefix prefixes_to_add in
attrs @ xmlns_attrs
in
[P.std_tag name attrs content]
- | Transclude transclusion -> render_transclusion forest transclusion
+ | Transclude transclusion -> render_transclusion ~env transclusion
| Contextual_number uri ->
let custom_number =
- let@ resource = Option.bind @@ forest.@{uri} in
+ let@ resource = Option.bind @@ env.forest.@{uri} in
match resource with
| T.Article article -> article.frontmatter.number
| _ -> None
@@ -218,12 +216,12 @@ and render_content_node (forest : State.t) (node : 'a T.content_node) :
[
X.uri_ "%s" @@ URI.to_string uri;
X.display_uri_ "%s"
- @@ URI.display_path_string ~base:forest.config.url uri;
+ @@ URI.display_path_string ~base:env.forest.config.url uri;
];
]
| Some num -> [P.txt "%s" num]
end
- | Link link -> render_link forest link
+ | Link link -> render_link ~env link
| Results_of_datalog_query q ->
let article_to_section =
T.article_to_section
@@ -236,22 +234,24 @@ and render_content_node (forest : State.t) (node : 'a T.content_node) :
metadata_shown = Some true;
}
in
- let results = Forest.run_datalog_query forest.graphs q in
- let@ article = List.map @~ Forest_util.get_sorted_articles forest results in
- render_section forest @@ article_to_section article
- | Section section -> [render_section forest section]
+ let results = Forest.run_datalog_query env.forest.graphs q in
+ let@ article =
+ List.map @~ Forest_util.get_sorted_articles env.forest results
+ in
+ render_section ~env @@ article_to_section article
+ | Section section -> [render_section ~env section]
| KaTeX (mode, content) ->
let display = match mode with Inline -> "inline" | Display -> "block" in
let body = Format.asprintf "%a" TeX_like.pp_content content in
[X.tex [X.display "%s" display] "" body]
- | Artefact resource -> [render_artefact forest resource]
+ | Artefact resource -> [render_artefact ~env resource]
| Datalog_script _ -> []
-and render_artefact forest (resource : T.content T.artefact) =
+and render_artefact ~env (resource : T.content T.artefact) =
X.resource
[X.hash "%s" resource.hash]
[
- X.resource_content [] @@ render_content forest resource.content;
+ X.resource_content [] @@ render_content ~env resource.content;
render_resource_sources resource.sources;
]
@@ -263,79 +263,81 @@ and render_resource_source source =
[X.type_ "%s" source.type_; X.resource_part "%s" source.part]
"" source.source
-and render_transclusion (forest : State.t) (transclusion : T.transclusion) :
- P.node list =
- match State.get_content_of_transclusion transclusion forest with
- | None -> Reporter.fatal (Resource_not_found transclusion.href)
- | Some content -> render_content forest content
+and render_transclusion ~env (transclusion : T.transclusion) : P.node list =
+ match State.get_content_of_transclusion transclusion env.forest with
+ | None ->
+ Reporter.fatal ?loc:(range ~env) (Resource_not_found transclusion.href)
+ | Some content -> render_content ~env content
-and render_link (forest : State.t) (link : T.content T.link) : P.node list =
- let article_opt = State.get_article link.href forest in
+and render_link ~env (link : T.content T.link) : P.node list =
+ let article_opt = State.get_article link.href env.forest in
let attrs =
match article_opt with
| None ->
- begin if not @@ In_backmatter.read () then
- match State.suggestion_for_uri link.href forest with
+ begin if not env.in_backmatter then
+ match State.suggestion_for_uri link.href env.forest with
| Ok -> ()
| Not_found {suggestion} ->
- Reporter.emit @@ Broken_link {uri = link.href; suggestion}
+ Reporter.emit ?loc:(range ~env)
+ @@ Broken_link {uri = link.href; suggestion}
end;
[
- X.href "%s" @@ URI.to_string @@ route forest link.href;
+ X.href "%s" @@ URI.to_string @@ route env.forest link.href;
X.type_ "external";
]
| Some article ->
[
- X.href "%s" @@ URI.to_string @@ route forest link.href;
+ X.href "%s" @@ URI.to_string @@ route env.forest link.href;
X.title_ "%s"
- @@ Plain_text_client.string_of_content ~forest ~router:(route forest)
- @@ State.get_expanded_title ?scope:(Scope.read ()) article.frontmatter
- forest;
+ @@ Plain_text_client.string_of_content ~forest:env.forest
+ ~router:(route env.forest)
+ @@ State.get_expanded_title ?scope:env.uri article.frontmatter
+ env.forest;
X.optional_ (X.uri_ "%s")
@@ Option.map URI.to_string article.frontmatter.uri;
X.optional_ (X.display_uri_ "%s")
@@ Option.map
- (URI.display_path_string ~base:forest.config.url)
+ (URI.display_path_string ~base:env.forest.config.url)
article.frontmatter.uri;
X.type_ "local";
]
in
- [X.link attrs @@ render_content forest link.content]
+ [X.link attrs @@ render_content ~env link.content]
-and render_attributions (forest : State.t) (scope : URI.t option)
+and render_attributions ~env (scope : URI.t option)
(primary_attributions : _ T.attribution list) =
X.authors []
- @@ List.map (render_attribution forest)
- @@ Forest_util.collect_attributions forest scope primary_attributions
+ @@ List.map (render_attribution ~env)
+ @@ Forest_util.collect_attributions env.forest scope primary_attributions
-and render_attribution forest (attrib : _ T.attribution) =
+and render_attribution ~env (attrib : _ T.attribution) =
let tag =
match attrib.role with Author -> X.author | Contributor -> X.contributor
in
- tag [] @@ render_attribution_vertex forest attrib.vertex
+ tag [] @@ render_attribution_vertex ~env attrib.vertex
-and render_attribution_vertex (forest : State.t) vtx =
+and render_attribution_vertex ~env vtx =
match vtx with
| T.Uri_vertex href ->
let content =
T.Content
[T.Transclude {href; target = Title {empty_when_untitled = false}}]
in
- render_link forest T.{href; content}
- | T.Content_vertex content -> render_content forest content
+ render_link ~env T.{href; content}
+ | T.Content_vertex content -> render_content ~env content
-and render_dates forest dates = X.null @@ List.map (render_date forest) dates
+and render_dates ~env dates = X.null @@ List.map (render_date ~env) dates
-and render_date forest (date : Human_datetime.t) =
- let config = forest.config in
+and render_date ~env (date : Human_datetime.t) =
+ let config = env.forest.config in
let href_attr =
let str =
Format.asprintf "%a" Human_datetime.pp (Human_datetime.drop_time date)
in
let uri = URI_scheme.named_uri ~base:config.url str in
- match State.get_article uri forest with
+ match State.get_article uri env.forest with
| None -> X.null_
- | Some _ -> X.href "%s" @@ URI.to_string @@ route forest uri
+ | Some _ -> X.href "%s" @@ URI.to_string @@ route env.forest uri
in
X.date [href_attr]
[
@@ -359,10 +361,15 @@ let render_article (forest : State.t) (article : T.content T.article) : P.node =
result
in
let config = forest.config in
- let@ () = Loop_detection.run in
- let@ () = Scope.run ~forest ~env:article.frontmatter.uri in
let@ xmlnss = Xmlns.run in
- let@ () = In_backmatter.run ~env:false in
+ let env =
+ {
+ forest;
+ in_backmatter = false;
+ uri = article.frontmatter.uri;
+ loops = Loop_detection.empty;
+ }
+ in
X.tree
begin
List.map render_xmlns_prefix xmlnss
@@ -376,16 +383,21 @@ let render_article (forest : State.t) (article : T.content T.article) : P.node =
]
end
[
- render_frontmatter forest article.frontmatter;
+ render_frontmatter ~env article.frontmatter;
X.mainmatter []
@@ begin
- let@ () = Loop_detection.add_seen_uri_opt article.frontmatter.uri in
- render_mainmatter forest @@ T.article_to_section article
+ render_mainmatter
+ ~env:
+ {
+ env with
+ loops =
+ Loop_detection.add_seen_uri_opt article.frontmatter.uri
+ env.loops;
+ }
+ @@ T.article_to_section article
end;
- (X.backmatter []
- @@
- let@ () = In_backmatter.run ~env:true in
- render_content forest article.backmatter);
+ X.backmatter []
+ @@ render_content ~env:{env with in_backmatter = true} article.backmatter;
]
let pp_xml ~(forest : State.t) ?stylesheet fmt (article : _ T.article) =
diff --git a/lib/frontend/Loop_detection.ml b/lib/frontend/Loop_detection.ml
new file mode 100644
index 0000000..be30413
--- /dev/null
+++ b/lib/frontend/Loop_detection.ml
@@ -0,0 +1,16 @@
+(*
+ * SPDX-FileCopyrightText: 2024 The Forester Project Contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *)
+
+open Forester_core
+
+type t = URI.Set.t
+let empty = URI.Set.empty
+let add_seen_uri = URI.Set.add
+let add_seen_uri_opt uri_opt =
+ match uri_opt with Some uri -> add_seen_uri uri | None -> Fun.id
+let have_seen_uri = URI.Set.mem
+let have_seen_uri_opt uri_opt =
+ match uri_opt with Some uri -> have_seen_uri uri | None -> fun _ -> false
diff --git a/lib/frontend/Loop_detection.mli b/lib/frontend/Loop_detection.mli
new file mode 100644
index 0000000..232089e
--- /dev/null
+++ b/lib/frontend/Loop_detection.mli
@@ -0,0 +1,8 @@
+open Forester_core
+
+type t
+val empty : t
+val add_seen_uri : URI.t -> t -> t
+val add_seen_uri_opt : URI.t option -> t -> t
+val have_seen_uri : URI.t -> t -> bool
+val have_seen_uri_opt : URI.t option -> t -> bool
diff --git a/lib/frontend/Loop_detection_effect.ml b/lib/frontend/Loop_detection_effect.ml
deleted file mode 100644
index e68d2bb..0000000
--- a/lib/frontend/Loop_detection_effect.ml
+++ /dev/null
@@ -1,25 +0,0 @@
-(*
- * SPDX-FileCopyrightText: 2024 The Forester Project Contributors
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- *)
-
-open Forester_core
-
-module Make () = struct
- open Algaeff.Reader.Make (struct
- type t = URI.Set.t
- end)
-
- let add_seen_uri uri = scope @@ URI.Set.add uri
-
- let add_seen_uri_opt uri_opt kont =
- match uri_opt with None -> kont () | Some uri -> add_seen_uri uri kont
-
- let have_seen_uri uri = URI.Set.mem uri @@ read ()
-
- let have_seen_uri_opt uri_opt =
- match uri_opt with None -> false | Some uri -> have_seen_uri uri
-
- let run k = run ~env:URI.Set.empty k
-end
diff --git a/lib/xml_names/Xmlns_effect.ml b/lib/xml_names/Xmlns_effect.ml
index 1ceab2d..f4f7ddd 100644
--- a/lib/xml_names/Xmlns_effect.ml
+++ b/lib/xml_names/Xmlns_effect.ml
@@ -24,6 +24,7 @@ module Xmlns_map = struct
}
end
+
module Make_writer (Elt : sig
type t
end) =