Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
DaanV2 committed Oct 16, 2023
2 parents 6d49c6f + 47ee6c0 commit 4faf89b
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 10 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
name: PR

on:
push: {}
pull_request: {}
workflow_dispatch: {}
push:
branches:
- main

jobs:
test:
Expand Down
4 changes: 1 addition & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
"version": "0.2.0",
"configurations": [
{
"type": "pwa-extensionHost",
"type": "extensionHost",
"request": "launch",
"name": "Launch Client",
"runtimeExecutable": "${execPath}",
"args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/client/out/**/*.js"],
"preLaunchTask": "npm watch 'vscode-plugin'"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Server",
"address": "localhost",
"protocol": "inspector",
"port": 6009,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/server/out/**/*.js"]
Expand Down
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);
}
258 changes: 258 additions & 0 deletions client/src/Commands/Docs/ShowDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { commands, ExtensionContext, FileType, ProgressLocation, Uri, window, workspace } from "vscode";
import { Commands } from "@blockception/shared";
import path from "path";
import { Console } from '../../Console/Console';

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
const item = sidebar.find((x) => x.processedTitle === title || x.toc_title === title);
if (!item) {
Console.Error("Failed to find docs item", title);
return;
}

return command.process(item);
});
}

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/";

/**
* Represents a command to show documentation.
*/
class ShowDocsCommand {
private storage: string;
// Cached flattened sidebar
private sidebar: SidebarItem[];

/**
* Creates a new instance of the ShowDocsCommand class.
* @param storage The storage path.
*/
constructor(storage: string) {
this.storage = storage;
this.sidebar = [];
}

/**
* Gets the flattened sidebar.
* @returns A promise that resolves to the flattened sidebar. or an empty array if the sidebar could not be downloaded.
*/
async getSidebar(): Promise<SidebarItem[]> {
if (this.sidebar.length > 0) {
return this.sidebar;
}
const data = await fetch(sidebar_url);

if (!data.ok) {
window.showErrorMessage("Failed to download docs sidebar");
return this.sidebar;
}

const jsonData = (await data.json()) as Sidebar;

if (!Sidebar.is(jsonData)) {
window.showErrorMessage("Failed to parse docs sidebar");
return this.sidebar;
}

return (this.sidebar = this.flattenSidebar(jsonData.items));
}

/**
* 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;
}

/**
* Gets the filepath for the specified sidebar item.
* @param item The sidebar item.
* @returns The filepath for the specified sidebar item.
*/
getFilepath(item: SidebarItem): string {
// At this point, elements without href are filtered out
return path.join(this.storage, item.href! + ".md");
}

/**
* Checks if the specified file can be read.
* @param filepath The filepath of the file to check.
* @returns A promise that resolves to true if the file can be read, false otherwise.
*/
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) {
Console.Error(`Failed to read file ${filepath}`, )
}

return false;
}

/**
* Downloads the specified URI to the specified filepath.
* @param uri The URI to download.
* @param filepath The filepath to download to.
* @returns A promise that resolves when the download is complete.
*/
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);

if (!result.ok) {
window.showErrorMessage("Failed to download docs\n", `${uri}\n${filepath}\n`, result.statusText);
return;
}

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] === undefined) {
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);

if (!mdResult.ok) {
window.showErrorMessage("Failed to download docs\n", `${uri}\n${filepath}\n`, mdResult.statusText);
return;
}

const mdText = await mdResult.text();

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

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

/**
* Processes the specified sidebar item.
* @param item The sidebar item to process.
* @returns A promise that resolves when the processing is complete.
*/
async process(item: SidebarItem): Promise<void> {
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;
}

namespace Sidebar {
export function is(item: any): item is Sidebar {
return item && item.items && Array.isArray(item.items);
}
}
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";
12 changes: 6 additions & 6 deletions client/src/Console/Console.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export class Console {
/**Sends a error to the console log of the server*/
static Error(message: string): void {
console.error(message);
static Error(message: string, ...optionalParams: any[]): void {
console.error(message, ...optionalParams);
}

/**Sends a error to the console log of the server*/
static Info(message: string): void {
console.info(message);
static Info(message: string, ...optionalParams: any[]): void {
console.info(message, ...optionalParams);
}

/**Sends a error to the console log of the server*/
Expand All @@ -15,7 +15,7 @@ export class Console {
}

/**Sends a error to the console log of the server*/
static Warn(message: string): void {
console.warn(message);
static Warn(message: string, ...optionalParams: any[]): void {
console.warn(message, ...optionalParams);
}
}
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 4faf89b

Please sign in to comment.