"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.looksLikeLinkToResource = exports.createWorkspaceLinkCache = exports.parseLocationInfoFromFragment = exports.MdLinkProvider = exports.LinkDefinitionSet = exports.ReferenceLinkMap = exports.MdLinkComputer = exports.htmlTagPathAttrs = exports.MdLinkKind = exports.resolveInternalDocumentLink = exports.HrefKind = void 0;
const l10n = require("@vscode/l10n");
const node_html_parser_1 = require("node-html-parser");
const vscode_uri_1 = require("vscode-uri");
const logging_1 = require("../logging");
const position_1 = require("../types/position");
const range_1 = require("../types/range");
const textDocument_1 = require("../types/textDocument");
const arrays_1 = require("../util/arrays");
const dispose_1 = require("../util/dispose");
const string_1 = require("../util/string");
const uri_1 = require("../util/uri");
const workspace_1 = require("../workspace");
const workspaceCache_1 = require("../workspaceCache");
var HrefKind;
(function (HrefKind) {
    HrefKind[HrefKind["External"] = 0] = "External";
    HrefKind[HrefKind["Internal"] = 1] = "Internal";
    HrefKind[HrefKind["Reference"] = 2] = "Reference";
})(HrefKind || (exports.HrefKind = HrefKind = {}));
function resolveInternalDocumentLink(sourceDocUri, linkText, workspace) {
    // Assume it must be an relative or absolute file path
    // Use a fake scheme to avoid parse warnings
    const tempUri = vscode_uri_1.URI.parse(`vscode-resource:${linkText}`);
    const docUri = workspace.getContainingDocument?.(sourceDocUri)?.uri ?? sourceDocUri;
    let resourceUri;
    if (!tempUri.path) {
        // Looks like a fragment only link
        if (typeof tempUri.fragment !== 'string') {
            return undefined;
        }
        resourceUri = sourceDocUri;
    }
    else if (tempUri.path[0] === '/') {
        const root = (0, workspace_1.getWorkspaceFolder)(workspace, docUri);
        if (root) {
            resourceUri = vscode_uri_1.Utils.joinPath(root, tempUri.path);
        }
    }
    else {
        if (docUri.scheme === 'untitled') {
            const root = (0, workspace_1.getWorkspaceFolder)(workspace, docUri);
            if (root) {
                resourceUri = vscode_uri_1.Utils.joinPath(root, tempUri.path);
            }
        }
        else {
            const base = vscode_uri_1.Utils.dirname(docUri);
            resourceUri = vscode_uri_1.Utils.joinPath(base, tempUri.path);
        }
    }
    if (!resourceUri) {
        return undefined;
    }
    return {
        resource: resourceUri,
        linkFragment: tempUri.fragment,
    };
}
exports.resolveInternalDocumentLink = resolveInternalDocumentLink;
var MdLinkKind;
(function (MdLinkKind) {
    MdLinkKind[MdLinkKind["Link"] = 1] = "Link";
    MdLinkKind[MdLinkKind["Definition"] = 2] = "Definition";
})(MdLinkKind || (exports.MdLinkKind = MdLinkKind = {}));
function createHref(sourceDocUri, link, workspace) {
    if (/^[a-z\-][a-z\-]+:/i.test(link)) {
        // Looks like a uri
        return { kind: HrefKind.External, uri: vscode_uri_1.URI.parse((0, uri_1.tryDecodeUri)(link)) };
    }
    const resolved = resolveInternalDocumentLink(sourceDocUri, link, workspace);
    if (!resolved) {
        return undefined;
    }
    return {
        kind: HrefKind.Internal,
        path: resolved.resource,
        fragment: resolved.linkFragment,
    };
}
function createMdLink(document, targetText, preHrefText, rawLink, matchIndex, fullMatch, workspace) {
    const isAngleBracketLink = rawLink.startsWith('<');
    const link = stripAngleBrackets(rawLink);
    let linkTarget;
    try {
        linkTarget = createHref((0, textDocument_1.getDocUri)(document), link, workspace);
    }
    catch {
        return undefined;
    }
    if (!linkTarget) {
        return undefined;
    }
    const pre = targetText + preHrefText;
    const linkStart = document.positionAt(matchIndex);
    const linkEnd = (0, position_1.translatePosition)(linkStart, { characterDelta: fullMatch.length });
    const targetStart = (0, position_1.translatePosition)(linkStart, { characterDelta: targetText.length });
    const targetRange = { start: targetStart, end: linkEnd };
    const hrefStart = (0, position_1.translatePosition)(linkStart, { characterDelta: pre.length + (isAngleBracketLink ? 1 : 0) });
    const hrefEnd = (0, position_1.translatePosition)(hrefStart, { characterDelta: link.length });
    const hrefRange = { start: hrefStart, end: hrefEnd };
    return {
        kind: MdLinkKind.Link,
        href: linkTarget,
        source: {
            hrefText: link,
            resource: (0, textDocument_1.getDocUri)(document),
            range: { start: linkStart, end: linkEnd },
            targetRange,
            hrefRange,
            isAngleBracketLink,
            ...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
        }
    };
}
function getFragmentRange(text, start, end) {
    const index = text.indexOf('#');
    if (index < 0) {
        return undefined;
    }
    return { start: (0, position_1.translatePosition)(start, { characterDelta: index + 1 }), end };
}
function getLinkSourceFragmentInfo(document, link, linkStart, linkEnd) {
    const fragmentRange = getFragmentRange(link, linkStart, linkEnd);
    return {
        pathText: document.getText({ start: linkStart, end: fragmentRange ? (0, position_1.translatePosition)(fragmentRange.start, { characterDelta: -1 }) : linkEnd }),
        fragmentRange,
    };
}
const angleBracketLinkRe = /^<(.*)>$/;
/**
 * Used to strip brackets from the markdown link
 *
 * <http://example.com> will be transformed to http://example.com
*/
function stripAngleBrackets(link) {
    return link.replace(angleBracketLinkRe, '$1');
}
/**
 * Matches `[text](link)` or `[text](<link>)`
 */
const linkPattern = new RegExp(
// text
(0, string_1.r) `(!?\[` + // open prefix match -->
    /**/ (0, string_1.r) `(?:` +
    /*****/ (0, string_1.r) `[^\[\]\\]|` + // Non-bracket chars, or...
    /*****/ (0, string_1.r) `\\.|` + // Escaped char, or...
    /*****/ (0, string_1.r) `\[[^\[\]]*\]` + // Matched bracket pair
    /**/ (0, string_1.r) `)*` +
    (0, string_1.r) `\])` + // <-- close prefix match
    // Destination
    (0, string_1.r) `(\(\s*)` + // Pre href
    /**/ (0, string_1.r) `(` +
    /*****/ (0, string_1.r) `[^\s\(\)\<](?:[^\s\(\)]|\([^\s\(\)]*?\))*|` + // Link without whitespace, or...
    /*****/ (0, string_1.r) `<(?:\\[<>]|[^<>])+>` + // In angle brackets
    /**/ (0, string_1.r) `)` +
    // Title
    /**/ (0, string_1.r) `\s*(?:"[^"]*"|'[^']*'|\([^\(\)]*\))?\s*` +
    (0, string_1.r) `\)`, 'g');
/**
* Matches `[text][ref]` or `[shorthand]` or `[shorthand][]`
*/
const referenceLinkPattern = new RegExp((0, string_1.r) `(^|[^\]\\])` + // Must not start with another bracket (workaround for lack of support for negative look behinds)
    (0, string_1.r) `(?:` +
    /**/ (0, string_1.r) `(?:` +
    /****/ (0, string_1.r) `(` + // Start link prefix
    /******/ (0, string_1.r) `!?` + // Optional image ref
    /******/ (0, string_1.r) `\[((?:` + // Link text
    /********/ (0, string_1.r) `\\\]|` + // escaped bracket, or...
    /********/ (0, string_1.r) `[^\[\]]|` + //non bracket char, or...
    /********/ (0, string_1.r) `\[[^\[\]]*\]` + // matched bracket pair
    /******/ `+)*)\]` + // end link  text
    /******/ (0, string_1.r) `\[\s*?` + // Start of link def
    /****/ (0, string_1.r) `)` + // end link prefix
    /****/ (0, string_1.r) `(` +
    /******/ (0, string_1.r) `[^\]]*?)\]` + //link def
    /******/ (0, string_1.r) `|` +
    /******/ (0, string_1.r) `\[\s*?([^\\\]]*?)\s*\])(?![\(])` +
    (0, string_1.r) `)`, 'gm');
/**
 * Matches `<http://example.com>`
 */
const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g;
/**
 * Matches `[text]: link`
 */
const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<(?:\\[<>]|[^<>])+>)/gm;
const inlineCodePattern = /(^|[^`])(`+)((?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\2)(?:$|[^`])/gm;
class NoLinkRanges {
    multiline;
    inline;
    static compute(tokens, document) {
        const multiline = tokens
            .filter(t => (t.type === 'code_block' || t.type === 'fence' || t.type === 'html_block') && !!t.map)
            .map(t => ({ type: t.type, range: t.map }));
        const inlineRanges = new Map();
        const text = document.getText();
        for (const match of text.matchAll(inlineCodePattern)) {
            const startOffset = (match.index ?? 0) + match[1].length;
            const startPosition = document.positionAt(startOffset);
            const range = { start: startPosition, end: document.positionAt(startOffset + match[3].length) };
            for (let line = range.start.line; line <= range.end.line; ++line) {
                let entry = inlineRanges.get(line);
                if (!entry) {
                    entry = [];
                    inlineRanges.set(line, entry);
                }
                entry.push(range);
            }
        }
        return new NoLinkRanges(multiline, inlineRanges);
    }
    constructor(
    /**
     * Block element ranges, such as code blocks. Represented by [line_start, line_end).
     */
    multiline, 
    /**
     * Inline code spans where links should not be detected
     */
    inline) {
        this.multiline = multiline;
        this.inline = inline;
    }
    contains(position, excludeType = '') {
        return this.multiline.some(({ type, range }) => type !== excludeType && position.line >= range[0] && position.line < range[1]) ||
            !!this.inline.get(position.line)?.some(inlineRange => (0, range_1.rangeContains)(inlineRange, position));
    }
    concatInline(inlineRanges) {
        const newInline = new Map(this.inline);
        for (const range of inlineRanges) {
            for (let line = range.start.line; line <= range.end.line; ++line) {
                let entry = newInline.get(line);
                if (!entry) {
                    entry = [];
                    newInline.set(line, entry);
                }
                entry.push(range);
            }
        }
        return new NoLinkRanges(this.multiline, newInline);
    }
}
/**
 * Map of html tags to attributes that contain links.
 */
exports.htmlTagPathAttrs = new Map([
    ['IMG', ['src']],
    ['VIDEO', ['src', 'placeholder']],
    ['SOURCE', ['src']],
    ['A', ['href']],
]);
/**
 * Stateless object that extracts link information from markdown files.
 */
class MdLinkComputer {
    #tokenizer;
    #workspace;
    constructor(tokenizer, workspace) {
        this.#tokenizer = tokenizer;
        this.#workspace = workspace;
    }
    async getAllLinks(document, token) {
        const tokens = await this.#tokenizer.tokenize(document);
        if (token.isCancellationRequested) {
            return [];
        }
        const noLinkRanges = NoLinkRanges.compute(tokens, document);
        const inlineLinks = Array.from(this.#getInlineLinks(document, noLinkRanges));
        return [
            ...inlineLinks,
            ...this.#getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))),
            ...this.#getLinkDefinitions(document, noLinkRanges),
            ...this.#getAutoLinks(document, noLinkRanges),
            ...this.#getHtmlLinks(document, noLinkRanges),
        ];
    }
    *#getInlineLinks(document, noLinkRanges) {
        const text = document.getText();
        for (const match of text.matchAll(linkPattern)) {
            const linkTextIncludingBrackets = match[1];
            const matchLinkData = createMdLink(document, linkTextIncludingBrackets, match[2], match[3], match.index ?? 0, match[0], this.#workspace);
            if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) {
                yield matchLinkData;
                // Also check for images in link text
                if (/\![\[\(]/.test(linkTextIncludingBrackets)) {
                    const linkText = linkTextIncludingBrackets.slice(1, -1);
                    const startOffset = (match.index ?? 0) + 1;
                    for (const innerMatch of linkText.matchAll(linkPattern)) {
                        const innerData = createMdLink(document, innerMatch[1], innerMatch[2], innerMatch[3], startOffset + (innerMatch.index ?? 0), innerMatch[0], this.#workspace);
                        if (innerData) {
                            yield innerData;
                        }
                    }
                    yield* this.#getReferenceLinksInText(document, linkText, startOffset, noLinkRanges);
                }
            }
        }
    }
    *#getAutoLinks(document, noLinkRanges) {
        const text = document.getText();
        const docUri = (0, textDocument_1.getDocUri)(document);
        for (const match of text.matchAll(autoLinkPattern)) {
            const linkOffset = (match.index ?? 0);
            const linkStart = document.positionAt(linkOffset);
            if (noLinkRanges.contains(linkStart)) {
                continue;
            }
            const link = match[1];
            const linkTarget = createHref(docUri, link, this.#workspace);
            if (!linkTarget) {
                continue;
            }
            const linkEnd = (0, position_1.translatePosition)(linkStart, { characterDelta: match[0].length });
            const hrefStart = (0, position_1.translatePosition)(linkStart, { characterDelta: 1 });
            const hrefEnd = (0, position_1.translatePosition)(hrefStart, { characterDelta: link.length });
            const hrefRange = { start: hrefStart, end: hrefEnd };
            yield {
                kind: MdLinkKind.Link,
                href: linkTarget,
                source: {
                    isAngleBracketLink: true,
                    hrefText: link,
                    resource: docUri,
                    targetRange: hrefRange,
                    hrefRange: hrefRange,
                    range: { start: linkStart, end: linkEnd },
                    ...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
                }
            };
        }
    }
    #getReferenceLinks(document, noLinkRanges) {
        const text = document.getText();
        return this.#getReferenceLinksInText(document, text, 0, noLinkRanges);
    }
    *#getReferenceLinksInText(document, text, startingOffset, noLinkRanges) {
        for (const match of text.matchAll(referenceLinkPattern)) {
            const linkStartOffset = startingOffset + (match.index ?? 0) + match[1].length;
            const linkStart = document.positionAt(linkStartOffset);
            if (noLinkRanges.contains(linkStart)) {
                continue;
            }
            let hrefStart;
            let hrefEnd;
            let reference = match[4];
            if (reference === '') { // [ref][],
                reference = match[3];
                if (!reference) {
                    continue;
                }
                const offset = linkStartOffset + 1;
                hrefStart = document.positionAt(offset);
                hrefEnd = document.positionAt(offset + reference.length);
            }
            else if (reference) { // [text][ref]
                const text = match[3];
                if (!text) {
                    // Handle the case ![][cat]
                    if (!match[2].startsWith('!')) {
                        // Empty links are not valid
                        continue;
                    }
                }
                if (!match[2].startsWith('!')) {
                    // Also get links in text
                    yield* this.#getReferenceLinksInText(document, match[3], linkStartOffset + 1, noLinkRanges);
                }
                const pre = match[2];
                const offset = linkStartOffset + pre.length;
                hrefStart = document.positionAt(offset);
                hrefEnd = document.positionAt(offset + reference.length);
            }
            else if (match[5]) { // [ref]
                reference = match[5];
                const offset = linkStartOffset + 1;
                hrefStart = document.positionAt(offset);
                const line = (0, textDocument_1.getLine)(document, hrefStart.line);
                // See if link looks like link definition
                if (linkStart.character === 0 && line[match[0].length - match[1].length] === ':') {
                    continue;
                }
                // See if link looks like a checkbox
                const checkboxMatch = line.match(/^\s*[\-\*]\s*\[x\]/i);
                if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) {
                    continue;
                }
                hrefEnd = document.positionAt(offset + reference.length);
            }
            else {
                continue;
            }
            const linkEnd = (0, position_1.translatePosition)(linkStart, { characterDelta: match[0].length - match[1].length });
            const hrefRange = { start: hrefStart, end: hrefEnd };
            yield {
                kind: MdLinkKind.Link,
                source: {
                    isAngleBracketLink: false,
                    hrefText: reference,
                    pathText: reference,
                    resource: (0, textDocument_1.getDocUri)(document),
                    range: { start: linkStart, end: linkEnd },
                    targetRange: hrefRange,
                    hrefRange: hrefRange,
                    fragmentRange: undefined,
                },
                href: {
                    kind: HrefKind.Reference,
                    ref: reference,
                }
            };
        }
    }
    *#getLinkDefinitions(document, noLinkRanges) {
        const text = document.getText();
        const docUri = (0, textDocument_1.getDocUri)(document);
        for (const match of text.matchAll(definitionPattern)) {
            const offset = (match.index ?? 0);
            const linkStart = document.positionAt(offset);
            if (noLinkRanges.contains(linkStart)) {
                continue;
            }
            const pre = match[1];
            const reference = match[2];
            const rawLinkText = match[3].trim();
            const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText);
            const linkText = stripAngleBrackets(rawLinkText);
            const target = createHref(docUri, linkText, this.#workspace);
            if (!target) {
                continue;
            }
            const hrefStart = (0, position_1.translatePosition)(linkStart, { characterDelta: pre.length + (isAngleBracketLink ? 1 : 0) });
            const hrefEnd = (0, position_1.translatePosition)(hrefStart, { characterDelta: linkText.length });
            const hrefRange = { start: hrefStart, end: hrefEnd };
            const refStart = (0, position_1.translatePosition)(linkStart, { characterDelta: 1 });
            const refRange = { start: refStart, end: (0, position_1.translatePosition)(refStart, { characterDelta: reference.length }) };
            const line = (0, textDocument_1.getLine)(document, linkStart.line);
            const linkEnd = (0, position_1.translatePosition)(linkStart, { characterDelta: line.length });
            yield {
                kind: MdLinkKind.Definition,
                source: {
                    isAngleBracketLink,
                    hrefText: linkText,
                    resource: docUri,
                    range: { start: linkStart, end: linkEnd },
                    targetRange: hrefRange,
                    hrefRange,
                    ...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd),
                },
                ref: { text: reference, range: refRange },
                href: target,
            };
        }
    }
    #getHtmlLinks(document, noLinkRanges) {
        const text = document.getText();
        if (!/<\w/.test(text)) { // Only parse if there may be html
            return [];
        }
        try {
            const tree = (0, node_html_parser_1.parse)(text);
            return this.#getHtmlLinksFromNode(document, tree, noLinkRanges);
        }
        catch {
            return [];
        }
    }
    static #toAttrEntry(attr) {
        return { attr, regexp: new RegExp(`(${attr}=["'])([^'"]*)["']`, 'i') };
    }
    static #linkAttrsByTag = new Map(Array.from(exports.htmlTagPathAttrs.entries(), ([key, value]) => [key, value.map(MdLinkComputer.#toAttrEntry)]));
    *#getHtmlLinksFromNode(document, node, noLinkRanges) {
        const attrs = MdLinkComputer.#linkAttrsByTag.get(node.tagName);
        if (attrs) {
            for (const attr of attrs) {
                const link = node.attributes[attr.attr];
                if (!link) {
                    continue;
                }
                const attrMatch = node.outerHTML.match(attr.regexp);
                if (!attrMatch) {
                    continue;
                }
                const docUri = (0, textDocument_1.getDocUri)(document);
                const linkTarget = createHref(docUri, link, this.#workspace);
                if (!linkTarget) {
                    continue;
                }
                const linkStart = document.positionAt(node.range[0] + attrMatch.index + attrMatch[1].length);
                if (noLinkRanges.contains(linkStart, 'html_block')) {
                    continue;
                }
                const linkEnd = (0, position_1.translatePosition)(linkStart, { characterDelta: attrMatch[2].length });
                const hrefRange = { start: linkStart, end: linkEnd };
                yield {
                    kind: MdLinkKind.Link,
                    href: linkTarget,
                    source: {
                        isAngleBracketLink: false,
                        hrefText: link,
                        resource: docUri,
                        targetRange: hrefRange,
                        hrefRange: hrefRange,
                        range: { start: linkStart, end: linkEnd },
                        ...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
                    }
                };
            }
        }
        for (const child of node.childNodes) {
            if (child instanceof node_html_parser_1.HTMLElement) {
                yield* this.#getHtmlLinksFromNode(document, child, noLinkRanges);
            }
        }
    }
}
exports.MdLinkComputer = MdLinkComputer;
class ReferenceLinkMap {
    #map = new Map();
    set(ref, link) {
        this.#map.set(this.#normalizeRefName(ref), link);
    }
    lookup(ref) {
        return this.#map.get(this.#normalizeRefName(ref));
    }
    has(ref) {
        return this.#map.has(this.#normalizeRefName(ref));
    }
    [Symbol.iterator]() {
        return this.#map.values();
    }
    /**
     * Normalizes a link reference. Link references are case-insensitive, so this lowercases the reference too so you can
     * correctly compare two normalized references.
     */
    #normalizeRefName(ref) {
        return ref.normalize().trim().toLowerCase();
    }
}
exports.ReferenceLinkMap = ReferenceLinkMap;
class LinkDefinitionSet {
    #map = new ReferenceLinkMap();
    constructor(links) {
        for (const link of links) {
            if (link.kind === MdLinkKind.Definition) {
                if (!this.#map.has(link.ref.text)) {
                    this.#map.set(link.ref.text, link);
                }
            }
        }
    }
    [Symbol.iterator]() {
        return this.#map[Symbol.iterator]();
    }
    lookup(ref) {
        return this.#map.lookup(ref);
    }
}
exports.LinkDefinitionSet = LinkDefinitionSet;
/**
 * Stateful object which provides links for markdown files the workspace.
 */
class MdLinkProvider extends dispose_1.Disposable {
    #linkCache;
    #linkComputer;
    #config;
    #workspace;
    #tocProvider;
    constructor(config, tokenizer, workspace, tocProvider, logger) {
        super();
        this.#config = config;
        this.#workspace = workspace;
        this.#tocProvider = tocProvider;
        this.#linkComputer = new MdLinkComputer(tokenizer, this.#workspace);
        this.#linkCache = this._register(new workspaceCache_1.MdDocumentInfoCache(this.#workspace, async (doc, token) => {
            logger.log(logging_1.LogLevel.Debug, 'LinkProvider.compute', { document: doc.uri, version: doc.version });
            const links = await this.#linkComputer.getAllLinks(doc, token);
            return {
                links,
                definitions: new LinkDefinitionSet(links),
            };
        }));
    }
    getLinks(document) {
        return this.#linkCache.getForDocument(document);
    }
    async provideDocumentLinks(document, token) {
        const { links, definitions } = await this.getLinks(document);
        if (token.isCancellationRequested) {
            return [];
        }
        return (0, arrays_1.coalesce)(links.map(data => this.#toValidDocumentLink(data, definitions)));
    }
    async resolveDocumentLink(link, token) {
        const href = this.#reviveLinkHrefData(link);
        if (!href) {
            return undefined;
        }
        const target = await this.#resolveInternalLinkTarget(href.path, href.fragment, token);
        switch (target.kind) {
            case 'folder':
                link.target = this.#createCommandUri('revealInExplorer', href.path);
                break;
            case 'external':
                link.target = target.uri.toString(true);
                break;
            case 'file':
                if (target.position) {
                    link.target = this.#createOpenAtPosCommand(target.uri, target.position);
                }
                else {
                    link.target = target.uri.toString(true);
                }
                break;
        }
        return link;
    }
    async resolveLinkTarget(linkText, sourceDoc, token) {
        const href = createHref(sourceDoc, linkText, this.#workspace);
        if (href?.kind !== HrefKind.Internal) {
            return undefined;
        }
        const resolved = resolveInternalDocumentLink(sourceDoc, linkText, this.#workspace);
        if (!resolved) {
            return undefined;
        }
        return this.#resolveInternalLinkTarget(resolved.resource, resolved.linkFragment, token);
    }
    async #resolveInternalLinkTarget(linkPath, linkFragment, token) {
        let target = linkPath;
        // If there's a containing document, don't bother with trying to resolve the
        // link to a workspace file as one will not exist
        const containingContext = this.#workspace.getContainingDocument?.(target);
        if (!containingContext) {
            const stat = await this.#workspace.stat(target);
            if (stat?.isDirectory) {
                return { kind: 'folder', uri: target };
            }
            if (token.isCancellationRequested) {
                return { kind: 'folder', uri: target };
            }
            if (!stat) {
                // We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
                let found = false;
                const dotMdResource = (0, workspace_1.tryAppendMarkdownFileExtension)(this.#config, target);
                if (dotMdResource) {
                    if (await this.#workspace.stat(dotMdResource)) {
                        target = dotMdResource;
                        found = true;
                    }
                }
                if (!found) {
                    return { kind: 'file', uri: target };
                }
            }
        }
        if (!linkFragment) {
            return { kind: 'file', uri: target };
        }
        // Try navigating with fragment that sets line number
        const locationLinkPosition = parseLocationInfoFromFragment(linkFragment);
        if (locationLinkPosition) {
            return { kind: 'file', uri: target, position: locationLinkPosition };
        }
        // Try navigating to header in file
        const doc = await this.#workspace.openMarkdownDocument(target);
        if (token.isCancellationRequested) {
            return { kind: 'file', uri: target };
        }
        if (doc) {
            const toc = await this.#tocProvider.getForContainingDoc(doc, token);
            const entry = toc.lookup(linkFragment);
            if (entry) {
                return { kind: 'file', uri: vscode_uri_1.URI.parse(entry.headerLocation.uri), position: entry.headerLocation.range.start, fragment: linkFragment };
            }
        }
        return { kind: 'file', uri: target };
    }
    #reviveLinkHrefData(link) {
        if (!link.data) {
            return undefined;
        }
        const mdLink = link.data;
        if (mdLink.href.kind !== HrefKind.Internal) {
            return undefined;
        }
        return { path: vscode_uri_1.URI.from(mdLink.href.path), fragment: mdLink.href.fragment };
    }
    #toValidDocumentLink(link, definitionSet) {
        switch (link.href.kind) {
            case HrefKind.External: {
                return {
                    range: link.source.hrefRange,
                    target: link.href.uri.toString(true),
                };
            }
            case HrefKind.Internal: {
                return {
                    range: link.source.hrefRange,
                    target: undefined,
                    tooltip: l10n.t('Follow link'),
                    data: link,
                };
            }
            case HrefKind.Reference: {
                // We only render reference links in the editor if they are actually defined.
                // This matches how reference links are rendered by markdown-it.
                const def = definitionSet.lookup(link.href.ref);
                if (!def) {
                    return undefined;
                }
                const target = this.#createOpenAtPosCommand(link.source.resource, def.source.hrefRange.start);
                return {
                    range: link.source.hrefRange,
                    tooltip: l10n.t('Go to link definition'),
                    target: target,
                    data: link
                };
            }
        }
    }
    #createCommandUri(command, ...args) {
        return `command:${command}?${encodeURIComponent(JSON.stringify(args))}`;
    }
    #createOpenAtPosCommand(resource, pos) {
        // If the resource itself already has a fragment, we need to handle opening specially 
        // instead of using `file://path.md#L123` style uris
        if (resource.fragment) {
            // Match the args of `vscode.open`
            return this.#createCommandUri('vscodeMarkdownLanguageservice.open', resource, {
                selection: (0, range_1.makeRange)(pos, pos),
            });
        }
        return resource.with({
            fragment: `L${pos.line + 1},${pos.character + 1}`
        }).toString(true);
    }
}
exports.MdLinkProvider = MdLinkProvider;
/**
 * Extract position info from link fragments that look like `#L5,3`
 */
function parseLocationInfoFromFragment(fragment) {
    const match = fragment.match(/^L(\d+)(?:,(\d+))?$/i);
    if (!match) {
        return undefined;
    }
    const line = +match[1] - 1;
    if (isNaN(line)) {
        return undefined;
    }
    const column = +match[2] - 1;
    return { line, character: isNaN(column) ? 0 : column };
}
exports.parseLocationInfoFromFragment = parseLocationInfoFromFragment;
function createWorkspaceLinkCache(parser, workspace) {
    const linkComputer = new MdLinkComputer(parser, workspace);
    return new workspaceCache_1.MdWorkspaceInfoCache(workspace, (doc, token) => linkComputer.getAllLinks(doc, token));
}
exports.createWorkspaceLinkCache = createWorkspaceLinkCache;
function looksLikeLinkToResource(configuration, href, targetResource) {
    if (href.path.fsPath === targetResource.fsPath) {
        return true;
    }
    return configuration.markdownFileExtensions.some(ext => href.path.with({ path: href.path.path + '.' + ext }).fsPath === targetResource.fsPath);
}
exports.looksLikeLinkToResource = looksLikeLinkToResource;
//# sourceMappingURL=documentLinks.js.map