From 6ddfbbecb339d913235e9b171cfa9d9b636e58e8 Mon Sep 17 00:00:00 2001 From: Bruno Paz <184563+brpaz@users.noreply.github.com> Date: Tue, 14 Jun 2022 09:49:24 +0100 Subject: [PATCH] feat: add documentation for Terraform and Web.dev and remove old GitHub documenation. --- README.md | 3 +- data/docsets.json | 33 +++++++++++++------ docsearch/extension.py | 1 + docsearch/listeners/query_listener.py | 2 +- docsearch/mapper/__init__.py | 3 +- docsearch/mapper/default.py | 5 +++ docsearch/mapper/prisma.py | 4 +++ docsearch/mapper/{github.py => terraform.py} | 15 ++++++--- docsearch/mapper/vercel.py | 4 +++ docsearch/mapper/webdev.py | 29 ++++++++++++++++ docsearch/searcher.py | 21 ++++++------ images/docs/terraform.png | Bin 0 -> 6797 bytes images/docs/webdev.png | Bin 0 -> 7476 bytes manifest.json | 21 ++++++++---- 14 files changed, 106 insertions(+), 35 deletions(-) rename docsearch/mapper/{github.py => terraform.py} (56%) create mode 100644 docsearch/mapper/webdev.py create mode 100644 images/docs/terraform.png create mode 100644 images/docs/webdev.png diff --git a/README.md b/README.md index f5384c8..2a5d5da 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ The following documentation sites are included by default in this extension: - Eslint - FluxCD - Gatsby -- GitHub (**Not working as it seems GitHub stopped using Algolia Docsearch**) - GitLab - GORM - Grafana @@ -49,6 +48,7 @@ The following documentation sites are included by default in this extension: - Supabase - Symfony - Tailwind +- Terraform - Typescript - Vercel - Vite @@ -59,6 +59,7 @@ The following documentation sites are included by default in this extension: - Vuex - Vue-Router - Webpack +- Web.dev ## Requirements diff --git a/data/docsets.json b/data/docsets.json index 9cb673f..06eae3b 100644 --- a/data/docsets.json +++ b/data/docsets.json @@ -330,15 +330,6 @@ "algolia_api_key": "8bca76b0664b04581dc9f9854e844a90", "url": "https://docs.helm.sh/docs" }, - "github": { - "name": "GitHub", - "description": "GitHub is where over 73 million developers shape the future of software, together.", - "icon": "images/docs/github.png", - "algolia_index": "github-docs-dotcom-en", - "algolia_application_id": "ZI5KPY1HBE", - "algolia_api_key": "685df617246c3a10abba589b4599288f", - "url": "https://docs.github.com" - }, "directus": { "name": "Directus", "description": "Directus is an open-source Headless CMS with the flexibility and power of a Data API", @@ -379,7 +370,7 @@ "icon": "images/docs/vercel.png", "algolia_index": "knowledge_production", "algolia_application_id": "NNTAHQI9C5", - "algolia_api_key": "ac5d89f9877f9fb09dbdc9a010cca761", + "algolia_api_key": "35b6e34b44ca4bcf530d0c4198f58fd8", "url": "https://vercel.com/", "facet_filters": [] }, @@ -459,5 +450,27 @@ "facet_filters": [ "version:current" ] + }, + "terraform": { + "name": "Terraform", + "description": "Terraform codifies cloud APIs into declarative configuration files.", + "icon": "images/docs/terraform.png", + "algolia_index": "product_TERRAFORM", + "algolia_application_id": "YY0FFNI7MF", + "algolia_api_key": "49e670100103ee846816a4b1a577004e", + "url": "https://www.terraform.io/docs", + "facet_filters": [] + }, + "webdev": { + "name": "Web.dev", + "description": "Take advantage of the latest modern technologies to build amazing web experiences for everyone.", + "icon": "images/docs/webdev.png", + "algolia_index": "prod_web_dev", + "algolia_application_id": "2JPAZHQ6K7", + "algolia_api_key": "ac32acde5503ed0ab18332e0592e9919", + "url": "https://web.dev/", + "facet_filters": [ + "locales:en" + ] } } diff --git a/docsearch/extension.py b/docsearch/extension.py index cb2e3c7..c2acc21 100644 --- a/docsearch/extension.py +++ b/docsearch/extension.py @@ -66,6 +66,7 @@ def search_in_docset(self, docset, query): try: results = self.searcher.search(docset, query) except Exception as e: + logger.error(e) return RenderResultListAction([ ExtensionResultItem( icon='images/icon.png', diff --git a/docsearch/listeners/query_listener.py b/docsearch/listeners/query_listener.py index 745049b..507d406 100644 --- a/docsearch/listeners/query_listener.py +++ b/docsearch/listeners/query_listener.py @@ -21,4 +21,4 @@ def on_event(self, event, extension): term = " ".join(query_parts[1:]) return extension.search_in_docset(docset, term) - return extension.show_docsets_list(event, query) + return extension.list_docsets(event, query) diff --git a/docsearch/mapper/__init__.py b/docsearch/mapper/__init__.py index 5cc94a1..60b29f3 100644 --- a/docsearch/mapper/__init__.py +++ b/docsearch/mapper/__init__.py @@ -1,4 +1,5 @@ from .default import DefaultMapper # noqa: F401 -from .github import GitHubMapper # noqa: F401 from .prisma import PrismaMapper # noqa: F401 from .vercel import VercelMapper # noqa: F401 +from .terraform import TerraformMapper # noqa: F401 +from .webdev import WebDevMapper # noqa: F401 diff --git a/docsearch/mapper/default.py b/docsearch/mapper/default.py index 72660f9..c747070 100644 --- a/docsearch/mapper/default.py +++ b/docsearch/mapper/default.py @@ -2,8 +2,13 @@ class DefaultMapper(): + + def get_type(self): + return "default" + def map(self, docset, hit): + print(hit) title, description = self.map_description(hit) if not description: diff --git a/docsearch/mapper/prisma.py b/docsearch/mapper/prisma.py index f022426..d25d44c 100644 --- a/docsearch/mapper/prisma.py +++ b/docsearch/mapper/prisma.py @@ -3,6 +3,10 @@ class PrismaMapper(DefaultMapper): """ Mapper for Algolia Docsearch response for Prisma documentation """ + + def get_type(self): + return 'prisma' + def map(self, docset, hit): """ Map function converts an item returned from the Algolia DocSearch to the format to be rendered in Ulauncher diff --git a/docsearch/mapper/github.py b/docsearch/mapper/terraform.py similarity index 56% rename from docsearch/mapper/github.py rename to docsearch/mapper/terraform.py index 2897752..ed3fe50 100644 --- a/docsearch/mapper/github.py +++ b/docsearch/mapper/terraform.py @@ -1,15 +1,20 @@ from .default import DefaultMapper -class GitHubMapper(DefaultMapper): - """ Mapper for the GitHub documentation """ +class TerraformMapper(DefaultMapper): + """ Mapper for Terraform response """ + + def get_type(self): + return 'terraform' + def map(self, docset, hit): """ Map function converts an item returned from the Algolia DocSearch to the format to be rendered in Ulauncher """ - title = hit["heading"] - description = hit["breadcrumbs"] - url = hit["url"] + title = hit["page_title"] + description = " -> ".join(hit["headings"]) + + url = "{}/{}".format(docset["url"], hit["objectID"]) return { 'url': url, diff --git a/docsearch/mapper/vercel.py b/docsearch/mapper/vercel.py index c87da49..b6ea46d 100644 --- a/docsearch/mapper/vercel.py +++ b/docsearch/mapper/vercel.py @@ -3,6 +3,10 @@ class VercelMapper(DefaultMapper): """ Mapper for the Vercel documentation """ + + def get_type(self): + return 'vercel' + def map(self, docset, hit): """ Map function converts an item returned from the Algolia DocSearch to the format to be rendered in Ulauncher diff --git a/docsearch/mapper/webdev.py b/docsearch/mapper/webdev.py new file mode 100644 index 0000000..832908e --- /dev/null +++ b/docsearch/mapper/webdev.py @@ -0,0 +1,29 @@ +from .default import DefaultMapper + +import re + + +class WebDevMapper(DefaultMapper): + """ Mapper for Terraform response """ + + def get_type(self): + return 'webdev' + + def map(self, docset, hit): + """ + Map function converts an item returned from the Algolia DocSearch to the format to be rendered in Ulauncher + """ + title = hit["_highlightResult"]["title"]["value"] + + url = "{}{}".format(docset["url"], hit["url"]) + + return { + 'url': url, + 'title': self.remove_html(title), + 'icon': docset['icon'], + 'category': url + } + + def remove_html(self, text): + regex = re.compile(r'<[^>]+>') + return regex.sub('', text) diff --git a/docsearch/searcher.py b/docsearch/searcher.py index 6f1e6a9..3ab97fa 100644 --- a/docsearch/searcher.py +++ b/docsearch/searcher.py @@ -5,7 +5,7 @@ import json import os import logging -from docsearch.mapper import DefaultMapper, GitHubMapper, VercelMapper, PrismaMapper +from docsearch.mapper import DefaultMapper, VercelMapper, PrismaMapper, TerraformMapper, WebDevMapper from algoliasearch.search_client import SearchClient from algoliasearch.exceptions import AlgoliaException @@ -29,6 +29,12 @@ def __init__(self): self.load_default_docsets() self.load_user_docsets() + self.results_mappers = [ + VercelMapper(), + TerraformMapper(), + PrismaMapper(), + WebDevMapper() + ] def load_default_docsets(self): """ Loads default docsets into memory """ @@ -101,7 +107,7 @@ def search(self, docset_key, term): try: search_results = index.search( term, self.get_search_request_options_for_docset(docset)) - print(search_results) + if not search_results['hits']: return [] @@ -114,14 +120,9 @@ def get_results_mapper(self, docset_key): """ Returns the mapper object that will map the specified docset data into the format required by the extension """ - if docset_key == "prisma": - return PrismaMapper() - - if docset_key == "github": - return GitHubMapper() - - if docset_key == "vercel": - return VercelMapper() + for mapper in self.results_mappers: + if mapper.get_type() == docset_key: + return mapper return DefaultMapper() diff --git a/images/docs/terraform.png b/images/docs/terraform.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4d3119c992ccd8da92344d289c3a41081780ab GIT binary patch literal 6797 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 literal 0 HcmV?d00001 diff --git a/images/docs/webdev.png b/images/docs/webdev.png new file mode 100644 index 0000000000000000000000000000000000000000..48cbaffe443e45813fc50ac391cb5d41956ba5cc GIT binary patch literal 7476 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