119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
|
/*---------------------------------------------------------------------------------------------
|
||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||
|
*--------------------------------------------------------------------------------------------*/
|
||
|
|
||
|
import * as vscode from 'vscode';
|
||
|
import { MarkdownEngine } from './markdownEngine';
|
||
|
import { Slug, githubSlugifier } from './slugify';
|
||
|
|
||
|
export interface TocEntry {
|
||
|
readonly slug: Slug;
|
||
|
readonly text: string;
|
||
|
readonly level: number;
|
||
|
readonly line: number;
|
||
|
readonly location: vscode.Location;
|
||
|
}
|
||
|
|
||
|
export interface SkinnyTextLine {
|
||
|
text: string;
|
||
|
}
|
||
|
|
||
|
export interface SkinnyTextDocument {
|
||
|
readonly uri: vscode.Uri;
|
||
|
readonly version: number;
|
||
|
readonly lineCount: number;
|
||
|
|
||
|
lineAt(line: number): SkinnyTextLine;
|
||
|
getText(): string;
|
||
|
}
|
||
|
|
||
|
export class TableOfContentsProvider {
|
||
|
private toc?: TocEntry[];
|
||
|
|
||
|
public constructor(
|
||
|
private engine: MarkdownEngine,
|
||
|
private document: SkinnyTextDocument
|
||
|
) { }
|
||
|
|
||
|
public async getToc(): Promise<TocEntry[]> {
|
||
|
if (!this.toc) {
|
||
|
try {
|
||
|
this.toc = await this.buildToc(this.document);
|
||
|
} catch (e) {
|
||
|
this.toc = [];
|
||
|
}
|
||
|
}
|
||
|
return this.toc;
|
||
|
}
|
||
|
|
||
|
public async lookup(fragment: string): Promise<TocEntry | undefined> {
|
||
|
const toc = await this.getToc();
|
||
|
const slug = githubSlugifier.fromHeading(fragment);
|
||
|
return toc.find(entry => entry.slug.equals(slug));
|
||
|
}
|
||
|
|
||
|
private async buildToc(document: SkinnyTextDocument): Promise<TocEntry[]> {
|
||
|
const toc: TocEntry[] = [];
|
||
|
const tokens = await this.engine.parse(document);
|
||
|
|
||
|
const existingSlugEntries = new Map<string, { count: number }>();
|
||
|
|
||
|
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||
|
const lineNumber = heading.map[0];
|
||
|
const line = document.lineAt(lineNumber);
|
||
|
|
||
|
let slug = githubSlugifier.fromHeading(line.text);
|
||
|
const existingSlugEntry = existingSlugEntries.get(slug.value);
|
||
|
if (existingSlugEntry) {
|
||
|
++existingSlugEntry.count;
|
||
|
slug = githubSlugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
|
||
|
} else {
|
||
|
existingSlugEntries.set(slug.value, { count: 0 });
|
||
|
}
|
||
|
|
||
|
toc.push({
|
||
|
slug,
|
||
|
text: TableOfContentsProvider.getHeaderText(line.text),
|
||
|
level: TableOfContentsProvider.getHeaderLevel(heading.markup),
|
||
|
line: lineNumber,
|
||
|
location: new vscode.Location(document.uri,
|
||
|
new vscode.Range(lineNumber, 0, lineNumber, line.text.length))
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Get full range of section
|
||
|
return toc.map((entry, startIndex): TocEntry => {
|
||
|
let end: number | undefined = undefined;
|
||
|
for (let i = startIndex + 1; i < toc.length; ++i) {
|
||
|
if (toc[i].level <= entry.level) {
|
||
|
end = toc[i].line - 1;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
const endLine = end ?? document.lineCount - 1;
|
||
|
return {
|
||
|
...entry,
|
||
|
location: new vscode.Location(document.uri,
|
||
|
new vscode.Range(
|
||
|
entry.location.range.start,
|
||
|
new vscode.Position(endLine, document.lineAt(endLine).text.length)))
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private static getHeaderLevel(markup: string): number {
|
||
|
if (markup === '=') {
|
||
|
return 1;
|
||
|
} else if (markup === '-') {
|
||
|
return 2;
|
||
|
} else { // '#', '##', ...
|
||
|
return markup.length;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static getHeaderText(header: string): string {
|
||
|
return header.replace(/^\s*#+\s*(.*?)\s*#*$/, (_, word) => word.trim());
|
||
|
}
|
||
|
}
|