|
@@ -1,334 +0,0 @@
|
|
|
-import * as fs from 'fs';
|
|
|
-import * as path from 'path';
|
|
|
-
|
|
|
-import * as MarkdownIt from 'markdown-it';
|
|
|
-import {
|
|
|
- githubSlugifier,
|
|
|
- resolveInternalDocumentLink,
|
|
|
- ExternalHref,
|
|
|
- FileStat,
|
|
|
- HrefKind,
|
|
|
- InternalHref,
|
|
|
- IMdLinkComputer,
|
|
|
- IMdParser,
|
|
|
- ITextDocument,
|
|
|
- IWorkspace,
|
|
|
- MdLink,
|
|
|
- MdLinkKind
|
|
|
-} from '@dsanders11/vscode-markdown-languageservice';
|
|
|
-import { Emitter, Range } from 'vscode-languageserver';
|
|
|
-import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
|
-import { URI } from 'vscode-uri';
|
|
|
-
|
|
|
-import { findMatchingFiles } from './utils';
|
|
|
-
|
|
|
-import type { Definition, ImageReference, Link, LinkReference } from 'mdast';
|
|
|
-import type { fromMarkdown as FromMarkdownFunction } from 'mdast-util-from-markdown';
|
|
|
-import type { Node, Position } from 'unist';
|
|
|
-import type { visit as VisitFunction } from 'unist-util-visit';
|
|
|
-
|
|
|
-// Helper function to work around import issues with ESM modules and ts-node
|
|
|
-// eslint-disable-next-line no-new-func
|
|
|
-const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
|
-
|
|
|
-// Helper function from `vscode-markdown-languageservice` codebase
|
|
|
-function tryDecodeUri (str: string): string {
|
|
|
- try {
|
|
|
- return decodeURI(str);
|
|
|
- } catch {
|
|
|
- return str;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Helper function from `vscode-markdown-languageservice` codebase
|
|
|
-function createHref (
|
|
|
- sourceDocUri: URI,
|
|
|
- link: string,
|
|
|
- workspace: IWorkspace
|
|
|
-): ExternalHref | InternalHref | undefined {
|
|
|
- if (/^[a-z-][a-z-]+:/i.test(link)) {
|
|
|
- // Looks like a uri
|
|
|
- return { kind: HrefKind.External, uri: URI.parse(tryDecodeUri(link)) };
|
|
|
- }
|
|
|
-
|
|
|
- const resolved = resolveInternalDocumentLink(sourceDocUri, link, workspace);
|
|
|
- if (!resolved) {
|
|
|
- return undefined;
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- kind: HrefKind.Internal,
|
|
|
- path: resolved.resource,
|
|
|
- fragment: resolved.linkFragment
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-function positionToRange (position: Position): Range {
|
|
|
- return {
|
|
|
- start: {
|
|
|
- character: position.start.column - 1,
|
|
|
- line: position.start.line - 1
|
|
|
- },
|
|
|
- end: { character: position.end.column - 1, line: position.end.line - 1 }
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-const mdIt = MarkdownIt({ html: true });
|
|
|
-
|
|
|
-export class MarkdownParser implements IMdParser {
|
|
|
- slugifier = githubSlugifier;
|
|
|
-
|
|
|
- async tokenize (document: TextDocument) {
|
|
|
- return mdIt.parse(document.getText(), {});
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export class DocsWorkspace implements IWorkspace {
|
|
|
- private readonly documentCache: Map<string, TextDocument>;
|
|
|
- readonly root: string;
|
|
|
-
|
|
|
- constructor (root: string) {
|
|
|
- this.documentCache = new Map();
|
|
|
- this.root = root;
|
|
|
- }
|
|
|
-
|
|
|
- get workspaceFolders () {
|
|
|
- return [URI.file(this.root)];
|
|
|
- }
|
|
|
-
|
|
|
- async getAllMarkdownDocuments (): Promise<Iterable<ITextDocument>> {
|
|
|
- const files = await findMatchingFiles(this.root, (file) =>
|
|
|
- file.endsWith('.md')
|
|
|
- );
|
|
|
-
|
|
|
- for (const file of files) {
|
|
|
- const document = TextDocument.create(
|
|
|
- URI.file(file).toString(),
|
|
|
- 'markdown',
|
|
|
- 1,
|
|
|
- fs.readFileSync(file, 'utf8')
|
|
|
- );
|
|
|
-
|
|
|
- this.documentCache.set(file, document);
|
|
|
- }
|
|
|
-
|
|
|
- return this.documentCache.values();
|
|
|
- }
|
|
|
-
|
|
|
- hasMarkdownDocument (resource: URI) {
|
|
|
- const relativePath = path.relative(this.root, resource.path);
|
|
|
- return (
|
|
|
- !relativePath.startsWith('..') &&
|
|
|
- !path.isAbsolute(relativePath) &&
|
|
|
- fs.existsSync(resource.path)
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- async openMarkdownDocument (resource: URI) {
|
|
|
- if (!this.documentCache.has(resource.path)) {
|
|
|
- const document = TextDocument.create(
|
|
|
- resource.toString(),
|
|
|
- 'markdown',
|
|
|
- 1,
|
|
|
- fs.readFileSync(resource.path, 'utf8')
|
|
|
- );
|
|
|
-
|
|
|
- this.documentCache.set(resource.path, document);
|
|
|
- }
|
|
|
-
|
|
|
- return this.documentCache.get(resource.path);
|
|
|
- }
|
|
|
-
|
|
|
- async stat (resource: URI): Promise<FileStat | undefined> {
|
|
|
- if (this.hasMarkdownDocument(resource)) {
|
|
|
- const stats = fs.statSync(resource.path);
|
|
|
- return { isDirectory: stats.isDirectory() };
|
|
|
- }
|
|
|
-
|
|
|
- return undefined;
|
|
|
- }
|
|
|
-
|
|
|
- async readDirectory (): Promise<Iterable<readonly [string, FileStat]>> {
|
|
|
- throw new Error('Not implemented');
|
|
|
- }
|
|
|
-
|
|
|
- //
|
|
|
- // These events are defined to fulfill the interface, but are never emitted
|
|
|
- // by this implementation since it's not meant for watching a workspace
|
|
|
- //
|
|
|
-
|
|
|
- #onDidChangeMarkdownDocument = new Emitter<ITextDocument>();
|
|
|
- onDidChangeMarkdownDocument = this.#onDidChangeMarkdownDocument.event;
|
|
|
-
|
|
|
- #onDidCreateMarkdownDocument = new Emitter<ITextDocument>();
|
|
|
- onDidCreateMarkdownDocument = this.#onDidCreateMarkdownDocument.event;
|
|
|
-
|
|
|
- #onDidDeleteMarkdownDocument = new Emitter<URI>();
|
|
|
- onDidDeleteMarkdownDocument = this.#onDidDeleteMarkdownDocument.event;
|
|
|
-}
|
|
|
-
|
|
|
-export class MarkdownLinkComputer implements IMdLinkComputer {
|
|
|
- private readonly workspace: IWorkspace;
|
|
|
-
|
|
|
- constructor (workspace: IWorkspace) {
|
|
|
- this.workspace = workspace;
|
|
|
- }
|
|
|
-
|
|
|
- async getAllLinks (document: ITextDocument): Promise<MdLink[]> {
|
|
|
- const { fromMarkdown } = (await dynamicImport(
|
|
|
- 'mdast-util-from-markdown'
|
|
|
- )) as { fromMarkdown: typeof FromMarkdownFunction };
|
|
|
-
|
|
|
- const tree = fromMarkdown(document.getText());
|
|
|
-
|
|
|
- const links = [
|
|
|
- ...(await this.#getInlineLinks(document, tree)),
|
|
|
- ...(await this.#getReferenceLinks(document, tree)),
|
|
|
- ...(await this.#getLinkDefinitions(document, tree))
|
|
|
- ];
|
|
|
-
|
|
|
- return links;
|
|
|
- }
|
|
|
-
|
|
|
- async #getInlineLinks (
|
|
|
- document: ITextDocument,
|
|
|
- tree: Node
|
|
|
- ): Promise<MdLink[]> {
|
|
|
- const { visit } = (await dynamicImport('unist-util-visit')) as {
|
|
|
- visit: typeof VisitFunction;
|
|
|
- };
|
|
|
-
|
|
|
- const documentUri = URI.parse(document.uri);
|
|
|
- const links: MdLink[] = [];
|
|
|
-
|
|
|
- visit(
|
|
|
- tree,
|
|
|
- (node) => node.type === 'link',
|
|
|
- (node: Node) => {
|
|
|
- const link = node as Link;
|
|
|
- const href = createHref(documentUri, link.url, this.workspace);
|
|
|
-
|
|
|
- if (href) {
|
|
|
- const range = positionToRange(link.position!);
|
|
|
-
|
|
|
- // NOTE - These haven't been implemented properly, but their
|
|
|
- // values aren't used for the link linting use-case
|
|
|
- const targetRange = range;
|
|
|
- const hrefRange = range;
|
|
|
- const fragmentRange = undefined;
|
|
|
-
|
|
|
- links.push({
|
|
|
- kind: MdLinkKind.Link,
|
|
|
- href,
|
|
|
- source: {
|
|
|
- hrefText: link.url,
|
|
|
- resource: documentUri,
|
|
|
- range,
|
|
|
- targetRange,
|
|
|
- hrefRange,
|
|
|
- fragmentRange,
|
|
|
- pathText: link.url.split('#')[0]
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- return links;
|
|
|
- }
|
|
|
-
|
|
|
- async #getReferenceLinks (
|
|
|
- document: ITextDocument,
|
|
|
- tree: Node
|
|
|
- ): Promise<MdLink[]> {
|
|
|
- const { visit } = (await dynamicImport('unist-util-visit')) as {
|
|
|
- visit: typeof VisitFunction;
|
|
|
- };
|
|
|
-
|
|
|
- const links: MdLink[] = [];
|
|
|
-
|
|
|
- visit(
|
|
|
- tree,
|
|
|
- (node) => ['imageReference', 'linkReference'].includes(node.type),
|
|
|
- (node: Node) => {
|
|
|
- const link = node as ImageReference | LinkReference;
|
|
|
- const range = positionToRange(link.position!);
|
|
|
-
|
|
|
- // NOTE - These haven't been implemented properly, but their
|
|
|
- // values aren't used for the link linting use-case
|
|
|
- const targetRange = range;
|
|
|
- const hrefRange = range;
|
|
|
-
|
|
|
- links.push({
|
|
|
- kind: MdLinkKind.Link,
|
|
|
- href: {
|
|
|
- kind: HrefKind.Reference,
|
|
|
- ref: link.label!
|
|
|
- },
|
|
|
- source: {
|
|
|
- hrefText: link.label!,
|
|
|
- resource: URI.parse(document.uri),
|
|
|
- range,
|
|
|
- targetRange,
|
|
|
- hrefRange,
|
|
|
- fragmentRange: undefined,
|
|
|
- pathText: link.label!
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- return links;
|
|
|
- }
|
|
|
-
|
|
|
- async #getLinkDefinitions (
|
|
|
- document: ITextDocument,
|
|
|
- tree: Node
|
|
|
- ): Promise<MdLink[]> {
|
|
|
- const { visit } = (await dynamicImport('unist-util-visit')) as {
|
|
|
- visit: typeof VisitFunction;
|
|
|
- };
|
|
|
-
|
|
|
- const documentUri = URI.parse(document.uri);
|
|
|
- const links: MdLink[] = [];
|
|
|
-
|
|
|
- visit(
|
|
|
- tree,
|
|
|
- (node) => node.type === 'definition',
|
|
|
- (node: Node) => {
|
|
|
- const definition = node as Definition;
|
|
|
- const href = createHref(documentUri, definition.url, this.workspace);
|
|
|
-
|
|
|
- if (href) {
|
|
|
- const range = positionToRange(definition.position!);
|
|
|
-
|
|
|
- // NOTE - These haven't been implemented properly, but their
|
|
|
- // values aren't used for the link linting use-case
|
|
|
- const targetRange = range;
|
|
|
- const hrefRange = range;
|
|
|
- const fragmentRange = undefined;
|
|
|
-
|
|
|
- links.push({
|
|
|
- kind: MdLinkKind.Definition,
|
|
|
- href,
|
|
|
- ref: {
|
|
|
- range,
|
|
|
- text: definition.label!
|
|
|
- },
|
|
|
- source: {
|
|
|
- hrefText: definition.url,
|
|
|
- resource: documentUri,
|
|
|
- range,
|
|
|
- targetRange,
|
|
|
- hrefRange,
|
|
|
- fragmentRange,
|
|
|
- pathText: definition.url.split('#')[0]
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- return links;
|
|
|
- }
|
|
|
-}
|