Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for search in odoc #972

Merged
merged 61 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9ea9f0f
Add folding for odoc's Lang model to the API.
panglesd Jun 20, 2023
0640643
Add an option to html generation to not include links
panglesd Jun 13, 2023
abac5a6
Add search support in odoc
panglesd Jun 20, 2023
ff95e3a
Adding search support in the reference driver
panglesd Jun 20, 2023
faa7b30
Allows generating json_display from parts.
EmileTrotignon Jun 12, 2023
85fb114
UI update
EmileTrotignon Jun 16, 2023
d5455b5
Format
EmileTrotignon Jun 16, 2023
c1351e6
UI fix
EmileTrotignon Jun 16, 2023
3db4a21
Ui fix support file
EmileTrotignon Jun 16, 2023
7ba7e5c
Add support for type search of extension constructors
EmileTrotignon Jun 20, 2023
d69df64
Type search of exceptions
EmileTrotignon Jul 5, 2023
b51461b
Adding a generator for search entries
panglesd Jul 7, 2023
95f7e9f
adding unused text search reference
panglesd Jul 11, 2023
e4a6246
prototype of asset reference resolving
panglesd Jul 11, 2023
9cc3b7a
Use asset reference for search scripts
panglesd Jul 12, 2023
18a9897
Removing now unused `--search-script` argument
panglesd Jul 12, 2023
a929d2e
Allow multiple search assets
panglesd Jul 12, 2023
681936c
Search : printing update
EmileTrotignon Jul 12, 2023
4a5f5e8
autoreview comments
panglesd Jul 13, 2023
bb8b7c8
Promote mdx tests
panglesd Jul 13, 2023
854706c
Sort json array in test to avoid dependent output
panglesd Jul 13, 2023
a72bc73
fix compatibility
panglesd Jul 13, 2023
358e8a5
Fix search-assets on pages, and subpages
panglesd Jul 24, 2023
c654327
Improve test
panglesd Jul 26, 2023
2eec6a7
Search index: Printing one entry per line in test
panglesd Aug 2, 2023
5ca72d3
Test index link
EmileTrotignon Aug 24, 2023
68769c1
style tweaking
EmileTrotignon Aug 25, 2023
667abee
Documents the new code
EmileTrotignon Aug 30, 2023
3c85973
Cleanup and comments
EmileTrotignon Aug 31, 2023
334f8cd
Use `Html_types` from `Tyxml` to define html type.
panglesd Aug 31, 2023
d9f6279
Update driver to use the `--search-asset` argument
panglesd Sep 3, 2023
d6d7302
Use record in reference driver
panglesd Sep 4, 2023
906c909
Search: Use type_expr from Text in Html
panglesd Sep 4, 2023
fb0bd65
remove extra div from json display
EmileTrotignon Sep 4, 2023
13e0167
comment update
EmileTrotignon Sep 4, 2023
1de1bb0
format
EmileTrotignon Sep 4, 2023
4d9f1d7
long rhs do not wrap
EmileTrotignon Sep 4, 2023
944992d
Search: report error when failing to generate an url
panglesd Sep 4, 2023
cfa9226
css update
EmileTrotignon Sep 4, 2023
37b3293
css tweaks
panglesd Oct 27, 2023
94e5649
separate some functions for jsoo deadcode elimination
EmileTrotignon Sep 7, 2023
85225f4
Review comments
panglesd Sep 13, 2023
3137b16
search: When no search assets are given, use the one from the parent
panglesd Sep 13, 2023
5b693d6
Separate "library" indexing from json indexing
panglesd Sep 13, 2023
8e96699
lazily load the search script
panglesd Sep 13, 2023
999538b
compile-index: take a file containing a list of file, as input
panglesd Sep 13, 2023
8c2fe50
Search: store search assets in Document and not in Page
panglesd Sep 13, 2023
0294701
search: improve script readability
panglesd Sep 13, 2023
b8e1dde
test: use `printf` instead of `echo -e`
panglesd Sep 13, 2023
16ea80f
Fixes sidebar on mobile
EmileTrotignon Sep 13, 2023
149ca5d
Search: improve search UI
panglesd Sep 14, 2023
73f7f12
search: Allow both --file-list and individual files in the CLI
panglesd Sep 14, 2023
81ee93f
Leaner html and css changes
EmileTrotignon Sep 20, 2023
78e6fa3
Search indexing: correct error message when passed a hidden module
panglesd Sep 29, 2023
da5aa1e
set search snake color
EmileTrotignon Sep 29, 2023
defe29d
Search: Pass search scripts at html generation
panglesd Oct 25, 2023
a25510d
Autoreview comments
panglesd Oct 26, 2023
e0b517a
Search: Driver: use --search-uri instead of assets
panglesd Oct 26, 2023
c3ab1ac
Search: Add changelog entry
panglesd Oct 26, 2023
9287208
Search: rename test folder
panglesd Oct 26, 2023
2fdbd16
Search: compatibility with new extension decl id
panglesd Oct 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Tags:

### Added
- Display 'private' keyword for private type extensions (@gpetiot, #1019)
- Add support for search (@panglesd, @EmileTrotignon, #972)

### Fixed

Expand Down
166 changes: 141 additions & 25 deletions doc/driver.mld
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,26 @@ let link ?(ignore_output = false) file =
if not ignore_output then
add_prefixed_output cmd link_output (Fpath.to_string file) lines

let html_generate ?(ignore_output = false) file source =
let html_generate ?(ignore_output = false) ?(assets = []) ?(search_uris = []) file source =
let open Cmd in
let source = match source with None -> empty | Some source -> v "--source" % p source in
let source =
match source with None -> empty | Some source -> v "--source" % p source
in
let assets =
List.fold_left
(fun acc filename -> acc % "--asset" % filename)
empty
assets
in
let search_uris =
List.fold_left
(fun acc filename -> acc % "--search-uri" % p filename)
empty
search_uris
in
let cmd =
odoc % "html-generate" %% source % p file % "-o" % "html" % "--theme-uri" % "odoc"
% "--support-uri" % "odoc"
odoc % "html-generate" %% source % p file %% assets %% search_uris % "-o" % "html"
% "--theme-uri" % "odoc" % "--support-uri" % "odoc"
in
let lines = run cmd in
if not ignore_output then
Expand All @@ -277,6 +291,7 @@ let support_files () =
run cmd
]}


We'll now make some library lists. We have not only external dependency
libraries, but [odoc] itself is also separated into libraries. These two
sets of libraries will be documented in different sections, so we'll keep them
Expand Down Expand Up @@ -318,7 +333,8 @@ let dep_libraries =
let odoc_libraries = [
"odoc_xref_test"; "odoc_xref2"; "odoc_odoc"; "odoc_html_support_files";
"odoc_model_desc"; "odoc_model"; "odoc_manpage"; "odoc_loader";
"odoc_latex"; "odoc_html"; "odoc_document"; "odoc_examples"; "odoc_parser"; "ocamlary" ];;
"odoc_latex"; "odoc_html"; "odoc_document"; "odoc_examples"; "odoc_parser";
"ocamlary"; "odoc_search" ; "odoc_html_frontend" ; "odoc_json_index" ];;

let all_libraries = dep_libraries @ odoc_libraries;;

Expand Down Expand Up @@ -429,6 +445,20 @@ let compile_deps f =
| _ -> Error (`Msg "odd")
]}

For each compiled odoc file, we'll need to remember some options given at
[odoc compile]-time. An example of this is the source code rendering: when we
enable the feature at compile time, we need to provide the source file at html
generation.

{[
type unit = {
file : Fpath.t;
ignore_output : bool;
source : Fpath.t option;
assets : string list;
}
]}

For [odoc] libraries, we infer the implementation and interface source file path
from the library name. We list them in a file, passed to [odoc source-tree], to
generate [src-source.odoc]. This file contains the source hierarchy, and will be
Expand Down Expand Up @@ -490,7 +520,7 @@ let compile_source_tree units =
let source_map = Fpath.v "source.map" in
let () = Bos.OS.File.write_lines source_map sources |> get_ok in
let () = source_tree ~parent:"odoc" ~output:odoc_source_tree source_map in
(odoc_source_tree, false, None)
{ file = odoc_source_tree ; ignore_output = false ; source = None ; assets = [] }

]}

Expand Down Expand Up @@ -533,6 +563,8 @@ let all_units =
Now we'll compile all of the parent [.mld] files. To ensure that the parents are compiled before the children, we start with [odoc.mld], then [deps.mld], and so on. The result of this file is a list of the resulting [odoc] files.

{[
let search_file = "index.js"

let compile_mlds () =
let mkpage x = "page-\"" ^ x ^ "\"" in
let mkmod x = "module-" ^ String.capitalize_ascii x in
Expand Down Expand Up @@ -563,9 +595,10 @@ let compile_mlds () =
"page-" ^ library ^ ".odoc")
all_libraries
in
{ file = Fpath.v "page-odoc.odoc" ; ignore_output = false ; source = None ; assets = [] } ::
List.map
(fun f -> (Fpath.v f, false, None))
("page-odoc.odoc" :: "page-deps.odoc" :: odocs @ extra_odocs)
(fun f -> { file = Fpath.v f ; ignore_output = false ; source = None; assets = [] })
( "page-deps.odoc" :: odocs @ extra_odocs)
]}

Now we get to the compilation phase. For each unit, we query its dependencies, then recursively call to compile these dependencies. Once this is done we compile the unit itself. If the unit has already been compiled we don't do anything. Note that we aren't checking the hashes of the dependencies which a build system should do to ensure that the module being compiled is the correct one. Again we benefit from the fact that we're creating the docs for one leaf package and that there must be no module name clashes in its dependencies. The result of this function is a list of the resulting [odoc] files.
Expand Down Expand Up @@ -600,42 +633,122 @@ let compile_all () =
let ignore_output = parent = "deps" in
let source_args = source_args impl in
compile file ~parent:lib ?source_args ~ignore_output [];
(output, ignore_output, impl) :: files
{ file = output ; ignore_output ; source = impl; assets = [] } :: files
in
source_tree
:: List.fold_left
(fun acc (parent, lib, dep, impl) ->
acc @ rec_compile ?impl parent lib (best_file dep))
[] all_units
@ mld_odocs
[] all_units
@ mld_odocs
]}

Linking is now straightforward. We link all [odoc] files.

{[
let link_all odoc_files =
List.map
(fun (odoc_file, ignore_output, source) ->
(fun ({ file = odoc_file ; ignore_output ; _ } as unit) ->
ignore (link ~ignore_output odoc_file);
Fpath.set_ext "odocl" odoc_file, source)
{ unit with file = Fpath.set_ext "odocl" odoc_file })
odoc_files
]}

Now we simply run [odoc html-generate] over all of the resulting [odocl] files.
This will generate sources, as well as documentation for non-hidden units.
We notify the generator that the javascript file to use for search is [index.js].

{[
let generate_all odocl_files =
let relativize_opt = function None -> None | Some file -> Some (relativize file) in
List.iter (fun (f, source) -> ignore(html_generate f (relativize_opt source))) odocl_files;
let search_uris = [Fpath.v "minisearch.js"; Fpath.v "index.js"] in
List.iter
(fun ({file = f ; ignore_output = _ ; source ; assets}) ->
ignore(html_generate ~assets ~search_uris f (relativize_opt source)))
odocl_files;
support_files ()
]}


Finally, we generate an index of all values, types, ... This index is meant to be consumed by search engines, to create their own index. It consists of a JSON array, containing entries with the name, full name, associated comment, link and anchor, and kind. Generating the index is done via [odoc compile-index], which create a json file.

Search engines written in OCaml can also call the [Odoc_model.Fold.unit] and [Odoc_model.Fold.page] function, in conjunction with [Odoc_search.Entry.entry_of_item] in order to get an OCaml value of each element to be indexed.

{[
let index_generate ?(ignore_output = false) () =
let open Cmd in
let files =
OS.Dir.contents (Fpath.v ".")
|> get_ok
|> List.filter (Fpath.has_ext "odocl")
|> List.filter (fun p -> not (String.equal "src-source.odocl" (Fpath.filename p)))
|> List.filter (fun p -> not (is_hidden p))
|> List.map Fpath.to_string
in
let index_map = Fpath.v "index.map" in
let () = Bos.OS.File.write_lines index_map files |> get_ok in
let cmd =
odoc % "compile-index" % "-o" % "html/index.json" % "--file-list"
% p index_map
in
let lines = run cmd in
if not ignore_output then
add_prefixed_output cmd generate_output "index compilation" lines
]}

We turn the JSON index into a javascript file. In order to never block the UI, this file will be used as a web worker by [odoc], to perform searches:

- The search query will be sent as a plain string to the web worker, using the standard mechanism of message passing
- The web worker has to sent back the result as a message to the main thread, containing the list of result. Each entry of this list must have the same form as it had in the original JSON file.
- The file must be given to the [odoc-support] URI.

In this driver, we use the minisearch javascript library. For more involved application, we could use [index.js] to call a server-side search engine via an API call.

{[
let js_index () =
let index = Bos.OS.File.read Fpath.(v "html" / "index.json") |> get_ok in
Bos.OS.File.writef (Fpath.v search_file) {|
let documents =
%s
;

let miniSearch = new MiniSearch({
fields: ['id', 'doc', 'entry_id'], // fields to index for full-text search
storeFields: ['display'], // fields to return with search results
idField: 'entry_id',
extractField: (document, fieldName) => {
if (fieldName === 'id') {
return document.id.map(e => e.kind + "-" + e.name).join('.')
}
return document[fieldName]
}
})


// Use a unique id since some entries' id are not unique (type extension or
// standalone doc comments for instance)
documents.forEach((entry,i) => entry.entry_id = i)
miniSearch.addAll(documents);

onmessage = (m) => {
let query = m.data;
let result = miniSearch.search(query);
postMessage(result.slice(0,200).map(a => a.display));
}
|} index |> get_ok ;
Bos.OS.Cmd.run Bos.Cmd.(v "cp" % search_file % "html/") |> get_ok;
Bos.OS.Cmd.run Bos.Cmd.(v "cp" % "minisearch.js" % "html/") |> get_ok;
]}



The following code executes all of the above, and we're done!

{[
let compiled = compile_all () in
let linked = link_all compiled in
let () = index_generate () in
let _ = js_index () in
generate_all linked
]}

Expand All @@ -647,9 +760,9 @@ Let's see if there was any output from the [odoc] invocations:
# !link_output;;
- : string list =
["";
"'../src/odoc/bin/main.exe' 'link' 'odoc_odoc.odoc' '-o' 'odoc_odoc.odocl' '-I' '.'";
"odoc_odoc.odoc: File \"src/odoc/fs.mli\", line 37, characters 6-73:";
"odoc_odoc.odoc: Warning: Failed to resolve reference unresolvedroot(Invalid_arg) Couldn't find \"Invalid_arg\"";
"'../src/odoc/bin/main.exe' 'link' 'odoc_html_frontend.odoc' '-o' 'odoc_html_frontend.odocl' '-I' '.'";
"odoc_html_frontend.odoc: File \"src/search/odoc_html_frontend.mli\", line 3, characters 15-25:";
"odoc_html_frontend.odoc: Warning: Failed to resolve reference unresolvedroot(Entry).t Couldn't find \"Entry\"";
"'../src/odoc/bin/main.exe' 'link' 'page-deps.odoc' '-o' 'page-deps.odocl' '-I' '.'";
"page-deps.odoc: File \"src/fmt.mli\", line 6, characters 4-13:";
"page-deps.odoc: Warning: Failed to resolve reference unresolvedroot(Format) Couldn't find \"Format\"";
Expand All @@ -674,17 +787,11 @@ Let's see if there was any output from the [odoc] invocations:
"page-deps.odoc: File \"src/fpath.mli\", line 7, characters 59-71:";
"page-deps.odoc: Warning: Failed to resolve reference unresolvedroot(Set) Couldn't find \"Set\"";
"page-deps.odoc: File \"src/fpath.mli\", line 7, characters 28-52:";
"page-deps.odoc: Warning: Failed to resolve reference unresolvedroot(file_exts) Couldn't find \"file_exts\"";
"'../src/odoc/bin/main.exe' 'link' 'page-odoc_for_authors.odoc' '-o' 'page-odoc_for_authors.odocl' '-I' '.'";
"page-odoc_for_authors.odoc: File \"odoc_for_authors.mld\", line 496, character 59 to line 497, character 18:";
"page-odoc_for_authors.odoc: Warning: Failed to resolve reference unresolvedroot(Features).canonical Couldn't find page \"Features\""]
"page-deps.odoc: Warning: Failed to resolve reference unresolvedroot(file_exts) Couldn't find \"file_exts\""]
# !source_tree_output;;
- : string list = [""]
# !generate_output;;
- : string list =
["";
"'../src/odoc/bin/main.exe' 'html-generate' 'odoc_examples.odocl' '-o' 'html' '--theme-uri' 'odoc' '--support-uri' 'odoc'";
"odoc_examples.odocl: Warning, resolved hidden path: Odoc_examples__.Unexposed.t"]
- : string list = [""]
]}

We can have a look at the produced hierarchy of files, which matches the desired output. Note that source files with a [.ml.html] extension are generated for modules compiled with the [--source] option.
Expand All @@ -707,13 +814,18 @@ odoc_document
odoc_examples
odoc_for_authors.html
odoc_html
odoc_html_frontend
odoc_html_support_files
odoc_json_index
odoc_latex
odoc_loader
odoc_manpage
odoc_model
odoc_model_desc
odoc_odoc
odoc_parser
odoc_search
odoc_search.js
odoc_xref2
odoc_xref_test
parent_child_spec.html
Expand Down Expand Up @@ -756,13 +868,17 @@ html/odoc/odoc_html/index.html
html/odoc/odoc_html/Odoc_html
html/odoc/odoc_html/Odoc_html/Config
html/odoc/odoc_html/Odoc_html/Config/index.html
html/odoc/odoc_html/Odoc_html_frontend
html/odoc/odoc_html/Odoc_html_frontend/index.html
html/odoc/odoc_html/Odoc_html/Generator
html/odoc/odoc_html/Odoc_html/Generator/index.html
html/odoc/odoc_html/Odoc_html/Html_fragment_json
html/odoc/odoc_html/Odoc_html/Html_fragment_json/index.html
html/odoc/odoc_html/Odoc_html/Html_page
html/odoc/odoc_html/Odoc_html/Html_page/index.html
html/odoc/odoc_html/Odoc_html/index.html
html/odoc/odoc_html/Odoc_html/Json
html/odoc/odoc_html/Odoc_html/Json/index.html
html/odoc/odoc_html/Odoc_html/Link
html/odoc/odoc_html/Odoc_html/Link/index.html
html/odoc/odoc_html/Odoc_html/Link/Path
Expand Down
1 change: 1 addition & 0 deletions doc/dune
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
(package odoc)
(universe) ; Benchmark depends on the running time of odoc commands
(glob_files *.mld)
(glob_files *.js)
(glob_files library_mlds/*)
(glob_files examples/*.ml*))
(action
Expand Down
3 changes: 3 additions & 0 deletions doc/library_mlds/odoc_html_frontend.mld
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{0 odoc_html_frontend}

{!childmodule-Odoc_html_frontend}
3 changes: 3 additions & 0 deletions doc/library_mlds/odoc_json_index.mld
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{0 odoc_json_index}

{!childmodule-Odoc_json_index}
3 changes: 3 additions & 0 deletions doc/library_mlds/odoc_search.mld
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{0 odoc_search}

{!childmodule-Odoc_search}
8 changes: 8 additions & 0 deletions doc/minisearch.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/odoc_for_authors.mld
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ examples below. They are:
- [@since string] - declares from which version the element has been available.
- [@version string] - declares the version of the element itself.
- [@canonical string] - declares the path to the canonical instance of this element.
Can be applied to modules, module types and types. See the {{!page-Features.canonical}
Can be applied to modules, module types and types. See the {{!page-features.canonical}
Language Features} page for more details.

The content of the tag is then the rest of the line, and it is uninterpreted,
Expand Down
4 changes: 4 additions & 0 deletions src/document/ML.mli
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ val source_page :
Lang.Source_info.infos ->
string ->
Types.Document.t

val type_expr : ?needs_parentheses:bool -> Lang.TypeExpr.t -> Codefmt.t

val record : Lang.TypeDecl.Field.t list -> Types.DocumentedSrc.one list
6 changes: 6 additions & 0 deletions src/document/generator.ml
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,8 @@ module Make (Syntax : SYNTAX) = struct

val extension : Lang.Extension.t -> Item.t

val record : Lang.TypeDecl.Field.t list -> DocumentedSrc.one list

val exn : Lang.Exception.t -> Item.t

val format_params :
Expand Down Expand Up @@ -1888,6 +1890,10 @@ module Make (Syntax : SYNTAX) = struct

include Page

let type_expr = type_expr

let record = record

let source_page id syntax_info infos source_code =
Document.Source_page (Source_page.source id syntax_info infos source_code)
end
4 changes: 4 additions & 0 deletions src/document/generator_signatures.ml
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,8 @@ module type GENERATOR = sig
Lang.Source_info.infos ->
string ->
Document.t

val type_expr : ?needs_parentheses:bool -> Lang.TypeExpr.t -> text

val record : Lang.TypeDecl.Field.t list -> DocumentedSrc.one list
end
Loading
Loading