From fb101fb98a25fec6a872c284490e60e1d3067076 Mon Sep 17 00:00:00 2001 From: AkshayWarrier <58233418+AkshayWarrier@users.noreply.github.com> Date: Sun, 10 Mar 2024 16:29:24 +0530 Subject: [PATCH] Add script to create index during build time (#98) --- build-aux/build-index.js | 303 ++++++++++++++++++++++++++++++++++++++ src/meson.build | 2 + src/sidebar/BrowseView.js | 303 -------------------------------------- src/sidebar/Sidebar.js | 62 +++++--- 4 files changed, 347 insertions(+), 323 deletions(-) create mode 100644 build-aux/build-index.js diff --git a/build-aux/build-index.js b/build-aux/build-index.js new file mode 100644 index 0000000..3c32f8a --- /dev/null +++ b/build-aux/build-index.js @@ -0,0 +1,303 @@ +#!/usr/bin/env -S gjs -m + +import Gio from "gi://Gio"; +import GLib from "gi://GLib"; + +Gio._promisify( + Gio.File.prototype, + "load_contents_async", + "load_contents_finish", +); + +Gio._promisify( + Gio.FileEnumerator.prototype, + "next_files_async", + "next_files_finish", +); + +Gio._promisify( + Gio.File.prototype, + "enumerate_children_async", + "enumerate_children_finish", +); + +Gio._promisify( + Gio.File.prototype, + "replace_contents_async", + "replace_contents_finish", +); + +// Biblioteca is GTK4 only; these are GTK 3 libraries +// we only support gi-docgen and gtk3 uses gtk-doc +const IGNORED_LIBRARIES = ["atk", "libhandy-1"]; + +const SECTION_TYPES = { + class: ["Classes", "#classes"], + content: ["Addition Documentation", "#extra"], + interface: ["Interfaces", "#interfaces"], + record: ["Structs", "#structs"], + alias: ["Aliases", "#aliases"], + enum: ["Enumerations", "#enums"], + bitfield: ["Bitfields", "#bitfields"], + function: ["Functions", "#functions"], + function_macro: ["Function Macros", "#function_macros"], + domain: ["Error Domains", "#domains"], + callback: ["Callbacks", "#callbacks"], + constant: ["Constants", "#constants"], +}; + +const SUBSECTION_TYPES = { + ctor: ["Constructors", "#constructors"], + type_func: ["Functions", "#type-functions"], + method: ["Instance Methods", "#methods"], + property: ["Properties", "#properties"], + signal: ["Signals", "#signals"], + class_method: ["Class Methods", "#class-methods"], + vfunc: ["Virtual Methods", "#virtual-methods"], +}; + +const REQUIRED = ["class", "interface", "record", "domain"]; +const DOC_INDEX = []; + +await loadDocs(); + +async function loadDocs() { + await Promise.all([ + scanLibraries(Gio.File.new_for_path("/app/share/doc")), + scanLibraries(Gio.File.new_for_path("/app/share/doc/glib-2.0")), + ]); + sort_index(DOC_INDEX); + + const [pkgdatadir] = ARGV; + GLib.mkdir_with_parents(pkgdatadir, 0o755); + + await Gio.File.new_for_path(pkgdatadir) + .get_child("doc-index.json") + .replace_contents_async( + new TextEncoder().encode(JSON.stringify(DOC_INDEX)), + null, + false, + Gio.FileCreateFlags.NONE, + null, + ); +} + +async function scanLibraries(base_dir) { + const libraries = []; + + const iter = await base_dir.enumerate_children_async( + "standard::name,standard::type", + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + null, + ); + + for (const info of iter) { + if (info.get_file_type() !== Gio.FileType.DIRECTORY) continue; + if (IGNORED_LIBRARIES.includes(info.get_name())) continue; + const file = iter.get_child(info); + libraries.push(loadLibrary(file).catch(console.error)); + } + + return Promise.allSettled(libraries).catch(console.error); +} + +async function loadLibrary(directory) { + try { + const json_file = directory.get_child("index.json"); + const html_file = directory.get_child("index.html"); + + const [data] = await json_file.load_contents_async(null); + const index = JSON.parse(decode(data)); + + const namespace = `${index.meta.ns}-${index.meta.version}`; + DOC_INDEX.push({ + name: namespace, + tag: "namespace", + search_name: namespace, + uri: html_file.get_uri(), + children: getChildren(index, directory), + }); + } catch (error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) throw error; + } +} + +function getChildren(index, dir) { + const index_html = dir.get_child("index.html").get_uri(); + const symbols = index.symbols; + + const sections = {}; + const subsections = {}; + + for (const section in SECTION_TYPES) sections[section] = []; + + for (const symbol of symbols) { + let location; + if (sections[symbol.type]) location = sections[symbol.type]; + else if (symbol.type_name) { + if (!subsections[symbol.type_name]) { + const new_subsection = {}; + for (const subsection in SUBSECTION_TYPES) + new_subsection[subsection] = []; + subsections[symbol.type_name] = new_subsection; + } + location = subsections[symbol.type_name][symbol.type]; + } + if (location) + location.push({ + name: symbol.name, + tag: getTagForDocument(symbol), + search_name: getSearchNameForDocument(symbol, index.meta), + uri: `${dir.get_uri()}/${getLinkForDocument(symbol)}`, + }); + } + + createSubsections(subsections, sections); + + const sections_model = []; + for (const section in sections) { + if (sections[section].length > 0) + sections_model.push({ + name: SECTION_TYPES[section][0], + uri: `${index_html}${SECTION_TYPES[section][1]}`, + children: sections[section], + }); + } + return sections_model; +} + +function createSubsections(subsections, sections) { + for (const type of REQUIRED) { + for (const item of sections[type]) { + const model = []; + const name = item.name; + for (const subsection in subsections[name]) { + if (subsections[name][subsection].length > 0) { + model.push({ + name: SUBSECTION_TYPES[subsection][0], + uri: `${item.uri}${SUBSECTION_TYPES[subsection][1]}`, + children: subsections[name][subsection], + }); + } + } + item.children = model; + } + } +} + +function sort_index(index) { + index.sort((a, b) => a.name.localeCompare(b.name)); + for (const item of index) { + if (item.children) { + sort_index(item.children); + } + } +} + +function decode(data) { + if (data instanceof GLib.Bytes) { + data = data.toArray(); + } + return new TextDecoder().decode(data); +} + +function getSearchNameForDocument(doc, meta) { + switch (doc.type) { + case "alias": + case "bitfield": + case "callback": + case "class": + case "domain": + case "enum": + case "interface": + case "record": + return doc.ctype; + + case "class_method": + case "constant": + case "ctor": + case "function": + case "function_macro": + case "method": + case "type_func": + return doc.ident; + + case "property": + return `${meta.ns}${doc.type_name}:${doc.name}`; + case "signal": + return `${meta.ns}${doc.type_name}::${doc.name}`; + case "vfunc": + return `${meta.ns}${doc.type_name}.${doc.name}`; + + case "content": + return doc.name; + } +} + +function getLinkForDocument(doc) { + switch (doc.type) { + case "alias": + return `alias.${doc.name}.html`; + case "bitfield": + return `flags.${doc.name}.html`; + case "callback": + return `callback.${doc.name}.html`; + case "class": + return `class.${doc.name}.html`; + case "class_method": + return `class_method.${doc.struct_for}.${doc.name}.html`; + case "constant": + return `const.${doc.name}.html`; + case "content": + return doc.href; + case "ctor": + return `ctor.${doc.type_name}.${doc.name}.html`; + case "domain": + return `error.${doc.name}.html`; + case "enum": + return `enum.${doc.name}.html`; + case "function": + return `func.${doc.name}.html`; + case "function_macro": + return `func.${doc.name}.html`; + case "interface": + return `iface.${doc.name}.html`; + case "method": + return `method.${doc.type_name}.${doc.name}.html`; + case "property": + return `property.${doc.type_name}.${doc.name}.html`; + case "record": + return `struct.${doc.name}.html`; + case "signal": + return `signal.${doc.type_name}.${doc.name}.html`; + case "type_func": + return `type_func.${doc.type_name}.${doc.name}.html`; + case "union": + return `union.${doc.name}.html`; + case "vfunc": + return `vfunc.${doc.type_name}.${doc.name}.html`; + } +} + +function getTagForDocument(doc) { + switch (doc.type) { + case "method": + case "class_method": + return "method"; + case "content": + return "additional"; + case "ctor": + return "constructor"; + case "domain": + return "error"; + case "function_macro": + return "macro"; + case "record": + return "struct"; + case "type_func": + return "function"; + default: + return doc.type; + } +} diff --git a/src/meson.build b/src/meson.build index beaec33..ffef8d5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,6 +8,8 @@ bin_conf.set('datadir', datadir) bin_conf.set('pkgdatadir', pkgdatadir) bin_conf.set('sourcedir', meson.project_source_root()) +meson.add_install_script('../build-aux/build-index.js', pkgdatadir) + blueprint_compiler = find_program( '/app/bin/blueprint-compiler', ) diff --git a/src/sidebar/BrowseView.js b/src/sidebar/BrowseView.js index b264320..f3e9eb7 100644 --- a/src/sidebar/BrowseView.js +++ b/src/sidebar/BrowseView.js @@ -4,43 +4,10 @@ import Gio from "gi://Gio"; import GLib from "gi://GLib"; import GObject from "gi://GObject"; import Webkit from "gi://WebKit"; - import DocumentationPage from "./DocumentationPage.js"; -import { decode } from "../util.js"; import Template from "./BrowseView.blp" with { type: "uri" }; -// Biblioteca is GTK4 only; these are GTK 3 libraries -// we only support gi-docgen and gtk3 uses gtk-doc -const IGNORED_LIBRARIES = ["atk", "libhandy-1"]; - -const SECTION_TYPES = { - class: ["Classes", "#classes"], - content: ["Addition Documentation", "#extra"], - interface: ["Interfaces", "#interfaces"], - record: ["Structs", "#structs"], - alias: ["Aliases", "#aliases"], - enum: ["Enumerations", "#enums"], - bitfield: ["Bitfields", "#bitfields"], - function: ["Functions", "#functions"], - function_macro: ["Function Macros", "#function_macros"], - domain: ["Error Domains", "#domains"], - callback: ["Callbacks", "#callbacks"], - constant: ["Constants", "#constants"], -}; - -const SUBSECTION_TYPES = { - ctor: ["Constructors", "#constructors"], - type_func: ["Functions", "#type-functions"], - method: ["Instance Methods", "#methods"], - property: ["Properties", "#properties"], - signal: ["Signals", "#signals"], - class_method: ["Class Methods", "#class-methods"], - vfunc: ["Virtual Methods", "#virtual-methods"], -}; - -const REQUIRED = ["class", "interface", "record", "domain"]; - const ITEM_HEIGHT = 38; class BrowseView extends Gtk.ScrolledWindow { @@ -49,7 +16,6 @@ class BrowseView extends Gtk.ScrolledWindow { this._sidebar = sidebar; this.root_model = Gio.ListStore.new(DocumentationPage); this.#createBrowseSelectionModel(); - this.#loadDocs().catch(console.error); this._scrolled_to = false; this._adj = this._browse_list_view.get_vadjustment(); @@ -186,254 +152,6 @@ class BrowseView extends Gtk.ScrolledWindow { }); this._browse_list_view.model = this.selection_model; } - - async #loadDocs() { - await Promise.all( - [ - this.#scanLibraries(Gio.File.new_for_path("/app/share/doc")), - this.#scanLibraries(Gio.File.new_for_path("/app/share/doc/glib-2.0")), - ].map((p) => p.catch(console.error)), - ); - this.emit("browse-view-loaded"); - } - - async #scanLibraries(base_dir) { - const libraries = []; - - const iter = await base_dir.enumerate_children_async( - "standard::name,standard::type", - Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - GLib.PRIORITY_DEFAULT, - null, - ); - // eslint-disable-next-line no-constant-condition - while (true) { - const infos = await iter.next_files_async( - 10, - GLib.PRIORITY_DEFAULT, - null, - ); - if (infos.length === 0) break; - - for (const info of infos) { - if (info.get_file_type() !== Gio.FileType.DIRECTORY) continue; - - if (IGNORED_LIBRARIES.includes(info.get_name())) continue; - - const directory = iter.get_child(info); - libraries.push(this.#loadLibrary(directory).catch(console.error)); - } - } - - return Promise.allSettled(libraries).catch(console.error); - } - - async #loadLibrary(directory) { - try { - const json_file = directory.get_child("index.json"); - const html_file = directory.get_child("index.html"); - - const [data] = await json_file.load_contents_async(null); - const index = JSON.parse(decode(data)); - - const namespace = `${index.meta.ns}-${index.meta.version}`; - const page = new DocumentationPage({ - name: namespace, - tag: "namespace", - search_name: namespace, - uri: html_file.get_uri(), - children: this.#getChildren(index, directory), - }); - - this.root_model.insert_sorted(page, this.#sortFunc); - // Dont move scrollbar while items are being inserted - this._adj.value = 0; - } catch (error) { - if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) - throw error; - } - } - - #getChildren(index, dir) { - const index_html = dir.get_child("index.html").get_uri(); - const symbols = index.symbols; - - const sections = {}; - const subsections = {}; - - for (const section in SECTION_TYPES) - sections[section] = this.#newListStore(); - - for (const symbol of symbols) { - let location; - if (sections[symbol.type]) location = sections[symbol.type]; - else if (symbol.type_name) { - if (!subsections[symbol.type_name]) { - const new_subsection = {}; - for (const subsection in SUBSECTION_TYPES) - new_subsection[subsection] = this.#newListStore(); - subsections[symbol.type_name] = new_subsection; - } - location = subsections[symbol.type_name][symbol.type]; - } - if (location) - location.insert_sorted( - new DocumentationPage({ - name: symbol.name, - tag: getTagForDocument(symbol), - search_name: getSearchNameForDocument(symbol, index.meta), - uri: `${dir.get_uri()}/${getLinkForDocument(symbol)}`, - }), - this.#sortFunc, - ); - } - - this.#createSubsections(subsections, sections); - - const sections_model = this.#newListStore(); - for (const section in sections) { - if (sections[section].get_n_items() > 0) - sections_model.insert_sorted( - new DocumentationPage({ - name: SECTION_TYPES[section][0], - uri: `${index_html}${SECTION_TYPES[section][1]}`, - children: sections[section], - }), - this.#sortFunc, - ); - } - return sections_model; - } - - #createSubsections(subsections, sections) { - for (const type of REQUIRED) { - for (const item of sections[type]) { - const model = this.#newListStore(); - const name = item.name; - for (const subsection in subsections[name]) { - if (subsections[name][subsection].get_n_items() > 0) { - model.insert_sorted( - new DocumentationPage({ - name: SUBSECTION_TYPES[subsection][0], - uri: `${item.uri}${SUBSECTION_TYPES[subsection][1]}`, - children: subsections[name][subsection], - }), - this.#sortFunc, - ); - } - } - item.children = model; - } - } - } - - #sortFunc(doc1, doc2) { - return doc1.name.localeCompare(doc2.name); - } - - #newListStore() { - return Gio.ListStore.new(DocumentationPage); - } -} - -function getSearchNameForDocument(doc, meta) { - switch (doc.type) { - case "alias": - case "bitfield": - case "callback": - case "class": - case "domain": - case "enum": - case "interface": - case "record": - return doc.ctype; - - case "class_method": - case "constant": - case "ctor": - case "function": - case "function_macro": - case "method": - case "type_func": - return doc.ident; - - case "property": - return `${meta.ns}${doc.type_name}:${doc.name}`; - case "signal": - return `${meta.ns}${doc.type_name}::${doc.name}`; - case "vfunc": - return `${meta.ns}${doc.type_name}.${doc.name}`; - - case "content": - return doc.name; - } -} - -function getLinkForDocument(doc) { - switch (doc.type) { - case "alias": - return `alias.${doc.name}.html`; - case "bitfield": - return `flags.${doc.name}.html`; - case "callback": - return `callback.${doc.name}.html`; - case "class": - return `class.${doc.name}.html`; - case "class_method": - return `class_method.${doc.struct_for}.${doc.name}.html`; - case "constant": - return `const.${doc.name}.html`; - case "content": - return doc.href; - case "ctor": - return `ctor.${doc.type_name}.${doc.name}.html`; - case "domain": - return `error.${doc.name}.html`; - case "enum": - return `enum.${doc.name}.html`; - case "function": - return `func.${doc.name}.html`; - case "function_macro": - return `func.${doc.name}.html`; - case "interface": - return `iface.${doc.name}.html`; - case "method": - return `method.${doc.type_name}.${doc.name}.html`; - case "property": - return `property.${doc.type_name}.${doc.name}.html`; - case "record": - return `struct.${doc.name}.html`; - case "signal": - return `signal.${doc.type_name}.${doc.name}.html`; - case "type_func": - return `type_func.${doc.type_name}.${doc.name}.html`; - case "union": - return `union.${doc.name}.html`; - case "vfunc": - return `vfunc.${doc.type_name}.${doc.name}.html`; - } -} - -function getTagForDocument(doc) { - switch (doc.type) { - case "method": - case "class_method": - return "method"; - case "content": - return "additional"; - case "ctor": - return "constructor"; - case "domain": - return "error"; - case "function_macro": - return "macro"; - case "record": - return "struct"; - case "type_func": - return "function"; - default: - return doc.type; - } } export default GObject.registerClass( @@ -449,28 +167,7 @@ export default GObject.registerClass( Webkit.WebView, ), }, - Signals: { - "browse-view-loaded": {}, - }, InternalChildren: ["browse_list_view"], }, BrowseView, ); - -Gio._promisify( - Gio.File.prototype, - "load_contents_async", - "load_contents_finish", -); - -Gio._promisify( - Gio.FileEnumerator.prototype, - "next_files_async", - "next_files_finish", -); - -Gio._promisify( - Gio.File.prototype, - "enumerate_children_async", - "enumerate_children_finish", -); diff --git a/src/sidebar/Sidebar.js b/src/sidebar/Sidebar.js index da04cbb..703c6b1 100644 --- a/src/sidebar/Sidebar.js +++ b/src/sidebar/Sidebar.js @@ -8,12 +8,13 @@ import ZoomButtons from "./ZoomButtons.js"; import BrowseView from "./BrowseView.js"; import SearchView from "./SearchView.js"; import DocumentationPage from "./DocumentationPage.js"; +import { decode } from "../util.js"; import Template from "./Sidebar.blp" with { type: "uri" }; import "../icons/edit-find-symbolic.svg"; -const gtk_index = 19; +const GTK_INDEX = 19; class Sidebar extends Adw.NavigationPage { constructor(...params) { @@ -25,7 +26,7 @@ class Sidebar extends Adw.NavigationPage { resetSidebar() { this.browse_view.collapseAllRows(); - this.browse_view.selection_model.selected = gtk_index; + this.browse_view.selection_model.selected = GTK_INDEX; this._search_entry.text = ""; this._stack.visible_child = this.browse_view; } @@ -38,6 +39,26 @@ class Sidebar extends Adw.NavigationPage { #initializeSidebar() { this.browse_view = new BrowseView(this); this.search_view = new SearchView(); + this.flattened_model = this.#newListStore(); + + const index_file = Gio.File.new_for_path(pkg.pkgdatadir).get_child( + "doc-index.json", + ); + const content = index_file.load_contents(null); + const doc_index = JSON.parse(decode(content[1])); + + let idx = 0; + const promises = []; + for (const item of doc_index) { + promises.push( + this.#buildPage(this.browse_view.root_model, item, [idx++]), + ); + } + + Promise.all(promises).then(() => { + this.browse_view.selection_model.selected = GTK_INDEX; + this.search_view.initializeModel(this.flattened_model); + }); this.browse_view.connect("notify::webview", () => { const webview_uri = this.browse_view.webview.uri; @@ -52,12 +73,6 @@ class Sidebar extends Adw.NavigationPage { this.browse_view.selectItem(path); }); - this.browse_view.connect("browse-view-loaded", () => { - this.flattened_model = this.#flattenModel(this.browse_view.root_model); - this.browse_view.selection_model.selected = gtk_index; - this.search_view.initializeModel(this.flattened_model); - }); - this._stack.add_child(this.browse_view); this._stack.add_child(this.search_view); this._stack.visible_child = this.browse_view; @@ -69,20 +84,27 @@ class Sidebar extends Adw.NavigationPage { popover.add_child(this.zoom_buttons, "zoom_buttons"); } - #flattenModel( - list_store, - flattened_model = Gio.ListStore.new(DocumentationPage), - path = [0], - ) { - for (const item of list_store) { - if (item.search_name) flattened_model.append(item); - if (item.children) { - this.#flattenModel(item.children, flattened_model, [...path, 1]); + async #buildPage(parent, item, path) { + const page = new DocumentationPage({ + name: item.name ?? null, + tag: item.tag ?? null, + search_name: item.search_name ?? null, + uri: item.uri ?? null, + children: item.children ? this.#newListStore() : null, + }); + this.uri_to_tree_path[item.uri] = path.slice(); + parent.append(page); + if (item.search_name) this.flattened_model.append(page); + if (item.children) { + let idx = 1; + for (const child of item.children) { + await this.#buildPage(page.children, child, [...path, idx++]); } - this.uri_to_tree_path[item.uri] = path.slice(); - path[path.length - 1]++; } - return flattened_model; + } + + #newListStore() { + return Gio.ListStore.new(DocumentationPage); } #connectSearchEntry() {