repos / neovimcraft

website that makes it easy to find neovim plugins
git clone https://github.com/neurosnap/neovimcraft.git

commit
321fa30
parent
2c1728f
author
Eric Bower
date
2022-07-01 03:35:22 +0000 UTC
Revert "Remove plugin page (#86)"

This reverts commit 1b6016774358fd2d2febd1bbc08f564354f68c0a.
7 files changed,  +308, -10
M package.json
+3, -2
 1@@ -14,14 +14,15 @@
 2     "scrape": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/scrape.ts",
 3     "patch": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/patch.ts",
 4     "process": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/process.ts",
 5+    "html": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/html.ts",
 6     "resource": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/resource.ts",
 7     "resource:clean": "node --no-warnings --es-module-specifier-resolution=node --loader ts-node/esm scripts/resource-clean.ts",
 8     "upload:clean": "gsutil -m rm -r gs://neovimcraft.com/*",
 9     "upload": "gsutil -m -h 'Cache-Control:private, max-age=0, no-transform' rsync -r ./build gs://neovimcraft.com",
10     "upload:db": "gsutil -m -h 'Cache-Control:private, max-age=0, no-transform' cp ./src/lib/db.json gs://neovimcraft.com/db.json",
11-    "build:all": "yarn scrape && yarn patch && yarn process && yarn build:clean && yarn build",
12+    "build:all": "yarn scrape && yarn patch && yarn process && yarn html && yarn build:clean && yarn build",
13     "deploy": "yarn build:clean && yarn build && yarn upload:clean && yarn upload && yarn upload:db",
14-    "deploy:all": "yarn scrape && yarn patch && yarn process && yarn deploy"
15+    "deploy:all": "yarn scrape && yarn patch && yarn process && yarn html && yarn deploy"
16   },
17   "devDependencies": {
18     "@sveltejs/adapter-static": "^1.0.0-next.13",
A scripts/html.ts
+54, -0
 1@@ -0,0 +1,54 @@
 2+import fs from 'fs';
 3+import util from 'util';
 4+import marked from 'marked';
 5+import prettier from 'prettier';
 6+
 7+import type { Plugin } from '../src/lib/types';
 8+
 9+const writeFile = util.promisify(fs.writeFile);
10+const readFile = util.promisify(fs.readFile);
11+
12+clean().catch(console.error);
13+
14+async function clean() {
15+  const file = await readFile('./src/lib/db.json', 'utf-8');
16+  const db = JSON.parse(file.toString());
17+  const markdownFile = await readFile('./src/lib/markdown.json', 'utf-8');
18+  const markdownDb = JSON.parse(markdownFile.toString());
19+
20+  const plugins = Object.values(db.plugins);
21+  const nextDb = {};
22+  plugins.forEach((plugin: Plugin) => {
23+    console.log(`processing ${plugin.id}`);
24+    marked.use({
25+      walkTokens: (token) => {
26+        const domain = 'https://github.com';
27+        const pre = `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
28+
29+        if (token.type === 'link' || token.type === 'image') {
30+          if (token.href && !token.href.startsWith('http') && !token.href.startsWith('#')) {
31+            token.href = `${pre}/${token.href.replace('./', ``)}`;
32+          }
33+        } else if (token.type === 'html') {
34+          token.text = '';
35+          // token.text = token.text.replace(/\.\//g, `${pre}/`);
36+        }
37+      },
38+    });
39+
40+    const markdown = markdownDb.markdown[plugin.id];
41+    if (!markdown) return;
42+    const html = marked(markdown);
43+    nextDb[plugin.id] = html;
44+  });
45+
46+  try {
47+    const json = prettier.format(JSON.stringify({ html: nextDb }), {
48+      parser: 'json',
49+      printWidth: 100,
50+    });
51+    await writeFile('./src/lib/html.json', json);
52+  } catch (err) {
53+    console.error(err);
54+  }
55+}
M scripts/process.ts
+50, -7
  1@@ -43,8 +43,11 @@ async function processMissingResources() {
  2   console.log(`Missing ${missing.length} resources`);
  3 
  4   const results = await processResources(missing);
  5+  const markdownFile = await readFile('./src/lib/markdown.json');
  6+  const markdownJson = JSON.parse(markdownFile.toString());
  7   const plugins = { ...db.plugins, ...results.plugins };
  8-  return plugins;
  9+  const markdown = { ...markdownJson.markdown, ...results.markdown };
 10+  return { plugins, markdown };
 11 }
 12 
 13 async function delay(ms: number): Promise<void> {
 14@@ -78,9 +81,9 @@ async function githubApi(endpoint: string): Promise<Resp<{ [key: string]: any }>
 15       ok: false,
 16       data: {
 17         status: res.status,
 18-        error: new Error(`JSON parsing error [${url}]`),
 19-      },
 20-    };
 21+        error: new Error(`JSON parsing error [${url}]`)
 22+      }
 23+    }
 24   }
 25 
 26   if (res.ok) {
 27@@ -99,6 +102,25 @@ async function githubApi(endpoint: string): Promise<Resp<{ [key: string]: any }>
 28   };
 29 }
 30 
 31+async function fetchReadme({ username, repo }: Props): Promise<Resp<string>> {
 32+  const result = await githubApi(`/repos/${username}/${repo}/readme`);
 33+  if (!result.ok) {
 34+    return {
 35+      ok: false,
 36+      data: result.data as any,
 37+    };
 38+  }
 39+
 40+  const url = result.data.download_url;
 41+  console.log(`Fetching ${url}`);
 42+  const readme = await fetch(url);
 43+  const data = await readme.text();
 44+  return {
 45+    ok: true,
 46+    data,
 47+  };
 48+}
 49+
 50 async function fetchRepo({ username, repo }: Props): Promise<Resp<{ [key: string]: any }>> {
 51   const result = await githubApi(`/repos/${username}/${repo}`);
 52   return result;
 53@@ -137,10 +159,18 @@ async function fetchGithubData(props: Props): Promise<Resp<any>> {
 54     console.log(`${branch.data.status}: ${branch.data.error.message}`);
 55   }
 56 
 57+  const readme = await fetchReadme({
 58+    username: props.username,
 59+    repo: props.repo,
 60+  });
 61+  if (readme.ok === false) {
 62+    console.log(`${readme.data.status}: ${readme.data.error.message}`);
 63+  }
 64+
 65   return {
 66     ok: true,
 67     data: {
 68-      readme: '',
 69+      readme: readme.ok ? readme.data : '',
 70       repo: repo.data,
 71       branch: branch.data,
 72     },
 73@@ -149,6 +179,7 @@ async function fetchGithubData(props: Props): Promise<Resp<any>> {
 74 
 75 async function processResources(resources: Resource[]) {
 76   const plugins: { [key: string]: Plugin } = {};
 77+  const markdown: { [key: string]: string } = {};
 78 
 79   console.log(`Fetching ${resources.length} resources`);
 80 
 81@@ -161,6 +192,7 @@ async function processResources(resources: Resource[]) {
 82         const resp = result.data;
 83         const id = `${d.username}/${d.repo}`;
 84 
 85+        markdown[id] = resp.readme;
 86         plugins[id] = createPlugin({
 87           id,
 88           username: d.username,
 89@@ -184,13 +216,24 @@ async function processResources(resources: Resource[]) {
 90     }
 91   }
 92 
 93-  return plugins;
 94+  return { plugins, markdown };
 95 }
 96 
 97-async function saveData(plugins: { [key: string]: Plugin }) {
 98+async function saveData({
 99+  plugins,
100+  markdown,
101+}: {
102+  plugins: { [key: string]: Plugin };
103+  markdown: { [key: string]: string };
104+}) {
105   const pluginJson = prettier.format(JSON.stringify({ plugins }), {
106     parser: 'json',
107     printWidth: 100,
108   });
109+  const markdownJson = prettier.format(JSON.stringify({ markdown }), {
110+    parser: 'json',
111+    printWidth: 100,
112+  });
113   await writeFile('./src/lib/db.json', pluginJson);
114+  await writeFile('./src/lib/markdown.json', markdownJson);
115 }
A src/lib/plugin-view.svelte
+97, -0
 1@@ -0,0 +1,97 @@
 2+<script lang="ts">
 3+  import qs from 'query-string';
 4+  import { goto } from '$app/navigation';
 5+
 6+  import type { Plugin, Tag } from './types';
 7+  import TagItem from './tag.svelte';
 8+  import Icon from './icon.svelte';
 9+  import Tooltip from '$lib/tooltip.svelte';
10+  import { format, relativeTimeFromDates } from '$lib/date';
11+
12+  export let plugin: Plugin;
13+  export let tags: Tag[];
14+  export let html: string = '<div>readme not found</div>';
15+
16+  function onSearch(curSearch: string) {
17+    const query = qs.parseUrl(window.location.search);
18+    const s = encodeURIComponent(curSearch);
19+    query.query.search = s;
20+    goto(`/${qs.stringifyUrl(query)}`);
21+  }
22+</script>
23+
24+<div class="meta">
25+  <div class="tags_view">
26+    {#each tags as tag}
27+      <TagItem {tag} {onSearch} showCount={false} />
28+    {/each}
29+  </div>
30+  <div class="metrics">
31+    <Tooltip tip="stars" bottom>
32+      <div class="metric"><Icon icon="star" /> <span>{plugin.stars}</span></div>
33+    </Tooltip>
34+    <Tooltip tip="open issues" bottom>
35+      <div class="metric"><Icon icon="alert-circle" /> <span>{plugin.openIssues}</span></div>
36+    </Tooltip>
37+    <Tooltip tip="subscribers" bottom>
38+      <div class="metric"><Icon icon="users" /> <span>{plugin.subscribers}</span></div>
39+    </Tooltip>
40+    <Tooltip tip="forks" bottom>
41+      <div class="metric"><Icon icon="git-branch" /> <span>{plugin.forks}</span></div>
42+    </Tooltip>
43+  </div>
44+  <div class="timestamps">
45+    <div>
46+      <h5 class="ts-header">CREATED</h5>
47+      <h2>{format(new Date(plugin.createdAt))}</h2>
48+    </div>
49+    <div>
50+      <h5 class="ts-header">UPDATED</h5>
51+      <h2>{relativeTimeFromDates(new Date(plugin.updatedAt))}</h2>
52+    </div>
53+  </div>
54+  <hr />
55+</div>
56+{@html html}
57+
58+<style>
59+  :global(img) {
60+    max-width: 100%;
61+    height: auto;
62+  }
63+
64+  .timestamps {
65+    display: flex;
66+    justify-content: space-between;
67+    background-color: var(--primary-color);
68+    padding: 15px;
69+    margin: 15px 0;
70+  }
71+
72+  .ts-header {
73+    margin-bottom: 10px;
74+  }
75+
76+  .meta {
77+    margin-bottom: 20px;
78+  }
79+
80+  .metrics {
81+    display: flex;
82+    justify-content: space-between;
83+  }
84+
85+  .metric {
86+    display: flex;
87+    align-items: center;
88+    cursor: pointer;
89+  }
90+
91+  .tags_view {
92+    margin-bottom: 10px;
93+  }
94+
95+  .install {
96+    margin-top: 10px;
97+  }
98+</style>
M src/lib/plugin.svelte
+4, -1
 1@@ -12,9 +12,12 @@
 2 <div class="container">
 3   <div class="header">
 4     <h2 class="item_header">
 5-      <a href="{plugin.link}">{plugin.repo}</a>
 6+      <a href="/plugin/{plugin.username}/{plugin.repo}">{plugin.repo}</a>
 7     </h2>
 8     <div class="metrics">
 9+      <Tooltip tip="github repo" bottom>
10+        <a href={plugin.link}><Icon icon="github" /></a>
11+      </Tooltip>
12       <Tooltip tip="stars" bottom>
13         <div class="metric-item"><Icon icon="star" /> <span>{plugin.stars}</span></div>
14       </Tooltip>
A src/routes/plugin/[username]/[repo].json.ts
+15, -0
 1@@ -0,0 +1,15 @@
 2+import type { Plugin } from '$lib/types';
 3+import * as db from '$lib/db.json';
 4+import * as pluginHtml from '$lib/html.json';
 5+import { derivePluginData } from '$lib/plugin-data';
 6+
 7+export async function get({ params }) {
 8+  const { username, repo } = params;
 9+  const id = `${username}/${repo}`;
10+  const plugin = db.plugins[id] as Plugin;
11+  const { tagDb } = derivePluginData(db.plugins);
12+  const html = pluginHtml.html[id];
13+  const tags = plugin.tags.map((t) => tagDb[t]).filter(Boolean);
14+
15+  return { body: { plugin, html, tags } };
16+}
A src/routes/plugin/[username]/[repo].svelte
+85, -0
 1@@ -0,0 +1,85 @@
 2+<script context="module" lang="ts">
 3+  export const prerender = true;
 4+
 5+  import type { LoadInput } from '@sveltejs/kit';
 6+
 7+  export async function load({ page, fetch }: LoadInput) {
 8+    const { username, repo } = page.params;
 9+    const url = `/plugin/${username}/${repo}.json`;
10+    const res = await fetch(url);
11+
12+    if (res.ok) {
13+      return {
14+        props: await res.json(),
15+      };
16+    }
17+
18+    return {
19+      status: res.status,
20+      error: new Error(`Could not load ${url}`),
21+    };
22+  }
23+</script>
24+
25+<script lang="ts">
26+  import type { Plugin, Tag } from '$lib/types';
27+  import PluginView from '$lib/plugin-view.svelte';
28+  import Icon from '$lib/icon.svelte';
29+  import Nav from '$lib/nav.svelte';
30+
31+  export let plugin: Plugin;
32+  export let tags: Tag[];
33+  export let html: string;
34+</script>
35+
36+<svelte:head>
37+  <title>
38+    {plugin.id}: {plugin.description}
39+  </title>
40+  <meta property="og:title" content={plugin.id} />
41+  <meta name="twitter:title" content={plugin.id} />
42+  <meta itemprop="name" content={plugin.id} />
43+
44+  <meta name="description" content="{plugin.id}: {plugin.description}" />
45+  <meta itemprop="description" content="{plugin.id}: {plugin.description}" />
46+  <meta property="og:description" content="{plugin.id}: {plugin.description}" />
47+  <meta name="twitter:description" content="{plugin.id}: {plugin.description}" />
48+</svelte:head>
49+
50+<Nav />
51+
52+<div class="container">
53+  <div class="view">
54+    <div class="header">
55+      <h1>{plugin.id}</h1>
56+      {#if plugin.homepage}<a href={plugin.homepage}>website</a>{/if}
57+      <a href={plugin.link}><Icon icon="github" /> <span>github</span></a>
58+    </div>
59+    <PluginView {plugin} {tags} {html} />
60+  </div>
61+</div>
62+
63+<style>
64+  .container {
65+    display: flex;
66+    justify-content: center;
67+    width: 100%;
68+  }
69+
70+  .view {
71+    max-width: 800px;
72+    width: 95%;
73+    padding: 0 10px 30px 10px;
74+  }
75+
76+  .header {
77+    display: flex;
78+    align-items: center;
79+  }
80+
81+  .header > a {
82+    margin-left: 15px;
83+    display: flex;
84+    align-items: center;
85+  }
86+</style>