Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
94 changes: 3 additions & 91 deletions home.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,7 @@
import { renderToString } from "preact-render-to-string";
import { PluginData, PluginsData, readInfoFile } from "./readInfoFile.js";
import { renderHomeHtml } from "./homeView.jsx";
import { readInfoFile } from "./readInfoFile.js";

export async function renderHome(origin: string, ctx?: ExecutionContext) {
const content = await renderContent(origin, ctx);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="Latest plugin versions for dprint." />
<title>Plugins - dprint</title>
<script>
addEventListener("load", () => {
for (const button of document.getElementsByClassName("copy-button")) {
button.addEventListener("click", () => {
// hack to copy to clipboard
const textArea = document.createElement("textarea");
textArea.value = button.dataset.url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
});
}
});
</script>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
${content}
</body>
</html>
`;
}

async function renderContent(origin: string, ctx?: ExecutionContext) {
const pluginsData = await readInfoFile(origin, ctx);
const section = (
<section id="content">
<h1>Plugins</h1>
{renderPlugins(pluginsData)}
<p>
Helpful commands:
<ul>
<li>
<code>dprint config update</code> - Automatically updates the plugins in a config file.
</li>
<li>
<code>dprint add</code> - Adds one of these plugins via a multi-select prompt.
</li>
<li>
<code>dprint add &lt;plugin-name&gt;</code> - Adds a plugin by name.
</li>
<li>
<code>dprint add &lt;gh-org&gt;/&lt;gh-repo&gt;</code> - Adds a plugin by GitHub repo.
</li>
</ul>
</p>
<p>
<a href="https://dprint.dev">Documentation</a>
</p>
</section>
);
return renderToString(section);
}

function renderPlugins(data: PluginsData) {
return (
<div id="plugins">
<div id="plugins-header">
<div>Name</div>
<div>Latest URL</div>
<div>Downloads (30d)</div>
<div></div>
</div>
{data.latest.map((plugin) => renderPlugin(plugin))}
</div>
);
}

function renderPlugin(plugin: PluginData) {
return (
<div className="plugin" key={plugin.name}>
<div>{plugin.name}</div>
<div>{plugin.url}</div>
<div>{plugin.downloadCount.allVersions?.toLocaleString("en-US")}</div>
<div>
<button type="button" className="copy-button" title="Copy to clipboard" data-url={plugin.url}>
Copy
</button>
</div>
</div>
);
return renderHomeHtml(pluginsData);
}
178 changes: 178 additions & 0 deletions homeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { renderToString } from "preact-render-to-string";
import type { PluginData, PluginsData } from "./readInfoFile.js";

// renders the full home page document for the given plugins data. kept free of
// any server/data imports so it can be rendered in isolation (e.g. previews).
export function renderHomeHtml(pluginsData: PluginsData) {
const content = renderToString(renderPage(pluginsData));
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#1e1e1e" />
<meta name="description" content="Latest plugin versions for dprint, the pluggable and configurable code formatting platform." />
<title>Plugins - dprint</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/style.css" />
<script>
addEventListener("load", () => {
let resetTimeout;
for (const button of document.getElementsByClassName("copy-button")) {
button.addEventListener("click", () => {
const url = button.dataset.url;
if (navigator.clipboard != null) {
navigator.clipboard.writeText(url).catch(() => {});
} else {
// fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
const original = button.textContent;
button.textContent = "copied ✓";
button.classList.add("copied");
clearTimeout(resetTimeout);
resetTimeout = setTimeout(() => {
button.textContent = original;
button.classList.remove("copied");
}, 1600);
});
}

// live filter
const search = document.getElementById("plugin-search");
if (search != null) {
const rows = document.getElementsByClassName("plugin-row");
const table = document.getElementsByClassName("plugin-table")[0];
const noMatches = document.getElementById("no-matches");
search.addEventListener("input", () => {
const query = search.value.trim().toLowerCase();
let visible = 0;
for (const row of rows) {
const match = query === "" || (row.dataset.search || "").indexOf(query) !== -1;
row.hidden = !match;
if (match) visible++;
}
if (table != null) table.hidden = visible === 0;
if (noMatches != null) noMatches.hidden = visible !== 0;
});
}
});
</script>
</head>
<body>
${content}
</body>
</html>
`;
}

function renderPage(pluginsData: PluginsData) {
return (
<main class="page">
<div class="topbar">
<input
type="search"
id="plugin-search"
class="plugin-search"
placeholder="Filter plugins…"
aria-label="Filter plugins"
autocomplete="off"
spellcheck={false}
/>
<a class="docs-link" href="https://dprint.dev/plugins">dprint plugin docs <span>↗</span></a>
</div>
{renderPlugins(pluginsData)}
<div id="no-matches" class="no-matches" hidden>No plugins match your filter.</div>
{renderCommands()}
</main>
);
}

// builds the lowercased string the search filters against: name, url, version,
// description, config key, and every file extension / file name / exec command
// the plugin handles.
function pluginSearchText(plugin: PluginData) {
const parts: (string | undefined)[] = [
plugin.name,
plugin.url,
plugin.version,
plugin.description,
plugin.configKey,
...(plugin.keywords ?? []),
...(plugin.fileExtensions ?? []),
...(plugin.fileNames ?? []),
];
for (const item of plugin.configItems ?? []) {
parts.push(...(item.match?.fileExtensions ?? []));
for (const command of item.config?.commands ?? []) {
parts.push(command.command, ...(command.exts ?? []));
}
}
return parts.filter(Boolean).join(" ").toLowerCase();
}

function renderCommands() {
const commands: { cmd: string; desc: string }[] = [
{ cmd: "dprint config update", desc: "Automatically updates the plugins in a config file." },
{ cmd: "dprint add", desc: "Adds one of these plugins via a multi-select prompt." },
{ cmd: "dprint add <plugin-name>", desc: "Adds a plugin by name." },
{ cmd: "dprint add <gh-org>/<gh-repo>", desc: "Adds a plugin by GitHub repo." },
];
return (
<section class="commands">
<h2>Helpful commands</h2>
<ul>
{commands.map((c) => (
<li key={c.cmd}>
<code>{c.cmd}</code> — {c.desc}
</li>
))}
</ul>
</section>
);
}

function renderPlugins(data: PluginsData) {
return (
<div class="plugin-table" role="table">
<div class="plugin-table-head" role="row">
<div role="columnheader">Plugin</div>
<div role="columnheader">Latest URL</div>
<div role="columnheader" class="num-col">Downloads (30d)</div>
<div role="columnheader"></div>
</div>
{data.latest.map((plugin) => renderPlugin(plugin))}
</div>
);
}

function renderPlugin(plugin: PluginData) {
return (
<div class="plugin-row" role="row" key={plugin.name} data-search={pluginSearchText(plugin)}>
<div class="col-name" role="cell">
<span class="swatch"></span>
<span class="name-text">{plugin.name}</span>
{plugin.version ? <span class="version-tag">{plugin.version}</span> : null}
</div>
<div class="col-url" role="cell">
<code>{plugin.url}</code>
</div>
<div class="col-downloads num-col" role="cell">
<span class="dl-label">Downloads (30d) </span>
{plugin.downloadCount.allVersions?.toLocaleString("en-US")}
</div>
<div class="col-action" role="cell">
<button type="button" class="copy-btn copy-button" title="Copy URL to clipboard" data-url={plugin.url}>
copy
</button>
</div>
</div>
);
}
Loading
Loading