Skip to content

Commit

Permalink
Add show docs command (#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
stirante authored Oct 16, 2023
1 parent d98bfc2 commit 7795920
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions client/src/Commands/Activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import * as Create from "./Create/Create";
import * as Errors from "./Errors/OpenLastest";
import * as Language from "./Language/Activate";
import * as ShowVanillaFile from "./Vanilla/ShowVanillaFile";
import * as ShowDocs from "./Docs/ShowDocs";

export function Activate(context: ExtensionContext): void {
Create.Activate(context);
Errors.Activate(context);
Language.Activate(context);
ShowVanillaFile.Activate(context);
ShowDocs.Activate(context);
}
194 changes: 194 additions & 0 deletions client/src/Commands/Docs/ShowDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { commands, ExtensionContext, FileType, ProgressLocation, Uri, window, workspace } from "vscode";
import { Commands } from "@blockception/shared";
import fetch, { RequestInit } from "node-fetch";

Check failure on line 3 in client/src/Commands/Docs/ShowDocs.ts

View workflow job for this annotation

GitHub Actions / 📦 Build Check

Cannot find module 'node-fetch' or its corresponding type declarations.
import path from "path";

export function Activate(context: ExtensionContext): void {
async function ShowDocs(args: any) {
const base = context.storageUri || context.globalStorageUri;
const storage_path = path.join(base.fsPath, "docs");
const command = new ShowDocsCommand(storage_path);

const sidebar = await command.getSidebar();

if (sidebar.length === 0) {
return;
}
// Processed titles are made from the toc_title and the parent toc_title
const titles: string[] = sidebar.map((x) => x.processedTitle ?? x.toc_title);

return window.showQuickPick(titles).then((title) => {
if (!title) {
return;
}

// Find the item by title or processed title
return command.process(sidebar.find((x) => x.processedTitle === title || x.toc_title === title));
});
}

context.subscriptions.push(commands.registerCommand(Commands.ShowDocs, ShowDocs));
}

const day_diff_2 = 1000 * 60 * 60 * 24 * 2;
// URL with the list of all docs
const sidebar_url = "https://learn.microsoft.com/en-us/minecraft/creator/toc.json";
// URL to the docs, that will be prepended to the href
const html_url = "https://learn.microsoft.com/en-us/minecraft/creator/";
// Cached flattened sidebar
let sidebar: SidebarItem[] | undefined;

class ShowDocsCommand {
private storage: string;

constructor(storage: string) {
this.storage = storage;
}

async getSidebar(): Promise<SidebarItem[]> {
if (sidebar) {
return Promise.resolve(sidebar);
}
const data = await fetch(sidebar_url);
const jsonData = (await data.json()) as Sidebar;
sidebar = this.flattenSidebar(jsonData.items);
return sidebar;
}

/**
* Flattens the sidebar from a tree to a list omitting all elements without href.
* It also adds the processed title to each element.
* @param sidebar The sidebar to flatten
* @param prefix The prefix to add to the processed title
* @returns The flattened sidebar
*/
flattenSidebar(sidebar: SidebarItem[], prefix: string = ""): SidebarItem[] {
const result: SidebarItem[] = [];

for (let I = 0; I < sidebar.length; I++) {
const item = sidebar[I];
// Add the prefix to the title
// Remove the docs title from the path, because it is too long
item.processedTitle = (prefix + "/" + item.toc_title).replace("/Minecraft: Bedrock Documentation/", "");
if (item.href) {
result.push(item);
}

if (item.children) {
// Iterative approach would be better, but recursion is easier
result.push(...this.flattenSidebar(item.children, item.processedTitle));
}
}

return result;
}

getFilepath(item: SidebarItem): string {
// At this point, elements without href are filtered out
return path.join(this.storage, item.href! + ".md");
}

async canRead(filepath: string): Promise<boolean> {
try {
const stat = await workspace.fs.stat(Uri.file(filepath));

if (stat.type !== FileType.File) return false;

//Check if the file is not older then 2 days
const now = new Date();
const file = new Date(stat.mtime);

const diff = now.getTime() - file.getTime();

return diff <= day_diff_2;
} catch (err) {
return false;
}
}

async download(uri: string, filepath: string): Promise<void> {
const progressOptions = {
location: ProgressLocation.Notification,
title: "Downloading docs",
cancellable: false,
};

return window.withProgress(progressOptions, async (progress) => {
const options: RequestInit = {
method: "GET",
};

progress.report({
message: "Downloading docs",
increment: 0,
});

try {
// Download the html page
const result = await fetch(uri, options);
const text = await result.text();

progress.report({ increment: 50 });
// Find the github link and change blob to raw. It's not the best solution, but it works
const matches = /(https:\/\/github.com\/MicrosoftDocs\/minecraft-creator\/blob\/main\/[^"]+)/g.exec(text);
if (!matches || matches.length === 0 || !matches[0]) {
window.showErrorMessage("Failed to download docs\n", uri + "\n", filepath + "\nNo github link found");
return;
}
const mdUrl = matches[0].replace(
"MicrosoftDocs/minecraft-creator/blob/",
"MicrosoftDocs/minecraft-creator/raw/"
);
// Download the markdown file
const mdResult = await fetch(mdUrl, options);
const mdText = await mdResult.text();

await workspace.fs.writeFile(Uri.file(filepath), Buffer.from(mdText, "utf8"));
} catch (err) {
window.showErrorMessage("Failed to download docs\n", uri + "\n", filepath + "\n", JSON.stringify(err));
}
console.log("Downloaded docs", filepath);

progress.report({ increment: 50 });
});
}

async process(item: SidebarItem | undefined): Promise<void> {
if (!item) {
return;
}
const filepath = this.getFilepath(item);

const dir = path.dirname(filepath);

await workspace.fs.createDirectory(Uri.file(dir));

const canRead = await this.canRead(filepath);
if (!canRead) {
const html_uri = html_url + item.href!;
await this.download(html_uri, filepath);
}

try {
const uri = Uri.file(filepath);
// Open the markdown preview
await commands.executeCommand("markdown.showPreview", uri);
} catch (err) {
window.showErrorMessage("Failed to open docs", filepath);
}
}
}

// Interfaces for the sidebar. It's not complete, but it's enough for this use case

interface Sidebar {
items: SidebarItem[];
}

interface SidebarItem {
href?: string;
toc_title: string;
children?: SidebarItem[];
// This is added by the flattenSidebar function
processedTitle?: string;
}
3 changes: 3 additions & 0 deletions client/src/Commands/Docs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* Auto generated */

export * from "./ShowDocs";
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@
"title": "Show vanilla file",
"command": "bc.minecraft.vanilla.show",
"category": "Blockception"
},
{
"title": "Show docs",
"command": "bc.minecraft.docs.show",
"category": "Blockception"
}
],
"grammars": [
Expand Down
3 changes: 3 additions & 0 deletions shared/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export namespace Commands {
/** */
export const ShowVanillaFile: string = "bc.minecraft.vanilla.show";

/** */
export const ShowDocs: string = "bc.minecraft.docs.show";

/** */
export namespace Files {
export const Append = "bc-files-append";
Expand Down

0 comments on commit 7795920

Please sign in to comment.