Skip to content

Commit

Permalink
refactor: Allow isInCodeSpan / isInFencedCodeBlock to accept string c…
Browse files Browse the repository at this point in the history
…ontent
  • Loading branch information
svsool committed Aug 5, 2020
1 parent fcafdc7 commit 232d3a9
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 12 deletions.
168 changes: 168 additions & 0 deletions src/utils/externalUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { window, workspace } from 'vscode';
import fs from 'fs';

import {
lineBreakOffsetsByLineIndex,
positionToOffset,
isInFencedCodeBlock,
isInCodeSpan,
isMdEditor,
isFileTooLarge,
cleanFileSizesCache,
} from './externalUtils';
import {
closeEditorsAndCleanWorkspace,
createFile,
openTextDocument,
rndName,
} from '../test/testUtils';

describe('lineBreakOffsetsByLineIndex()', () => {
it('should return offset for a single empty line', () => {
expect(lineBreakOffsetsByLineIndex('')).toEqual([1]);
});

it('should return offset for multiline string', () => {
expect(lineBreakOffsetsByLineIndex('test\r\ntest\rtest\ntest')).toEqual([6, 16, 21]);
});
});

describe('positionToOffset()', () => {
it('should through with illegal arguments', () => {
expect(() => positionToOffset('', { line: -1, column: 0 })).toThrowError();
expect(() => positionToOffset('', { line: 0, column: -1 })).toThrowError();
});

it('should transform position to offset', () => {
expect(positionToOffset('', { line: 0, column: 0 })).toEqual(0);
expect(positionToOffset('test\r\ntest\rtest\ntest', { line: 1, column: 0 })).toEqual(6);
expect(positionToOffset('test\r\ntest\rtest\ntest', { line: 2, column: 0 })).toEqual(16);
});

it('should handle line and column overflow properly', () => {
expect(positionToOffset('', { line: 10, column: 10 })).toEqual(0);
});
});

describe('isInFencedCodeBlock()', () => {
it('should return false when within outside of fenced code block', async () => {
expect(isInFencedCodeBlock('\n```Fenced code block```', 0)).toBe(false);
});

it('should return true when within fenced code block', async () => {
expect(isInFencedCodeBlock(`\n\`\`\`\nFenced code block\n\`\`\``, 2)).toBe(true);
});
});

describe('isInCodeSpan()', () => {
it('should return false when outside of code span', async () => {
expect(isInCodeSpan(' `test`', 0, 0)).toBe(false);
});

it('should return true when within code span', async () => {
expect(isInCodeSpan('`test`', 0, 1)).toBe(true);
});

it('should return true when within code span on the next line', async () => {
expect(isInCodeSpan('\n`test`', 1, 1)).toBe(true);
});
});

describe('isMdEditor()', () => {
beforeEach(closeEditorsAndCleanWorkspace);

afterEach(closeEditorsAndCleanWorkspace);

it('should return false when editor is not markdown', async () => {
const doc = await workspace.openTextDocument({ language: 'html', content: '' });
const editor = await window.showTextDocument(doc);

expect(isMdEditor(editor)).toBe(false);
});

it('should return true when editor is for markdown', async () => {
const doc = await workspace.openTextDocument({ language: 'markdown', content: '' });
const editor = await window.showTextDocument(doc);

expect(isMdEditor(editor)).toBe(true);
});
});

describe('isFileTooLarge()', () => {
beforeEach(closeEditorsAndCleanWorkspace);

afterEach(closeEditorsAndCleanWorkspace);

it('should return false when editor language other than markdown', async () => {
const doc = await workspace.openTextDocument({ language: 'html', content: '' });
const editor = await window.showTextDocument(doc);

expect(isMdEditor(editor)).toBe(false);
});

it('should return true when editor language is markdown', async () => {
const doc = await workspace.openTextDocument({ language: 'markdown', content: '' });
const editor = await window.showTextDocument(doc);

expect(isMdEditor(editor)).toBe(true);
});
});

describe('isFileTooLarge()', () => {
beforeEach(async () => {
await closeEditorsAndCleanWorkspace();
cleanFileSizesCache();
});

afterEach(async () => {
await closeEditorsAndCleanWorkspace();
cleanFileSizesCache();
});

it('should return false if file does not exist', async () => {
const doc = await workspace.openTextDocument({ content: '' });
expect(isFileTooLarge(doc)).toBe(false);
});

it('should not call statSync and use cached file size', async () => {
const name = rndName();

await createFile(`${name}.md`, 'test');

const doc = await openTextDocument(`${name}.md`);

const fsStatSyncSpy = jest.spyOn(fs, 'statSync');

isFileTooLarge(doc, 100);

expect(fsStatSyncSpy).toBeCalledTimes(1);

fsStatSyncSpy.mockClear();

isFileTooLarge(doc, 100);

expect(fsStatSyncSpy).not.toBeCalled();

fsStatSyncSpy.mockRestore();
});

it('should return false when file is not too large', async () => {
const name = rndName();

await createFile(`${name}.md`, 'test');

const doc = await openTextDocument(`${name}.md`);

expect(isFileTooLarge(doc, 100)).toBe(false);
});

it('should return true when file is too large', async () => {
const name = rndName();

await createFile(`${name}.md`, 'test'.repeat(10));

const doc = await openTextDocument(`${name}.md`);

expect(isFileTooLarge(doc, 35)).toBe(true);
});
});
76 changes: 64 additions & 12 deletions src/utils/externalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Position, Range, TextDocument, TextEditor } from 'vscode';
import fs from 'fs';

/*
These utils borrowed from https:/yzhang-gh/vscode-markdown
Some of these utils borrowed from https:/yzhang-gh/vscode-markdown
*/

export function isMdEditor(editor: TextEditor) {
Expand All @@ -11,18 +11,64 @@ export function isMdEditor(editor: TextEditor) {

export const REGEX_FENCED_CODE_BLOCK = /^( {0,3}|\t)```[^`\r\n]*$[\w\W]+?^( {0,3}|\t)``` *$/gm;

export const isInFencedCodeBlock = (doc: TextDocument, lineNum: number): boolean => {
let textBefore = doc.getText(new Range(new Position(0, 0), new Position(lineNum, 0)));
textBefore = textBefore.replace(REGEX_FENCED_CODE_BLOCK, '').replace(/<!--[\W\w]+?-->/g, '');
const REGEX_CODE_SPAN = /`[^`]*?`/gm;

export const lineBreakOffsetsByLineIndex = (value: string): number[] => {
const result = [];
let index = value.indexOf('\n');

while (index !== -1) {
result.push(index + 1);
index = value.indexOf('\n', index + 1);
}

result.push(value.length + 1);

return result;
};

export const positionToOffset = (content: string, position: { line: number; column: number }) => {
if (position.line < 0) {
throw new Error('Illegal argument: line must be non-negative');
}

if (position.column < 0) {
throw new Error('Illegal argument: column must be non-negative');
}

const lineBreakOffsetsByIndex = lineBreakOffsetsByLineIndex(content);
if (lineBreakOffsetsByIndex[position.line] !== undefined) {
return (lineBreakOffsetsByIndex[position.line - 1] || 0) + position.column || 0;
}

return 0;
};

export const isInFencedCodeBlock = (
documentOrContent: TextDocument | string,
lineNum: number,
): boolean => {
const content =
typeof documentOrContent === 'string' ? documentOrContent : documentOrContent.getText();
const textBefore = content
.slice(0, positionToOffset(content, { line: lineNum, column: 0 }))
.replace(REGEX_FENCED_CODE_BLOCK, '')
.replace(/<!--[\W\w]+?-->/g, '');
// So far `textBefore` should contain no valid fenced code block or comment
return /^( {0,3}|\t)```[^`\r\n]*$[\w\W]*$/gm.test(textBefore);
};

const REGEX_CODE_SPAN = /`[^`]*?`/gm;

export const isInCodeSpan = (doc: TextDocument, lineNum: number, offset: number): boolean => {
let textBefore = doc.getText(new Range(new Position(0, 0), new Position(lineNum, offset)));
textBefore = textBefore.replace(REGEX_CODE_SPAN, '').trim();
export const isInCodeSpan = (
documentOrContent: TextDocument | string,
lineNum: number,
offset: number,
): boolean => {
const content =
typeof documentOrContent === 'string' ? documentOrContent : documentOrContent.getText();
const textBefore = content
.slice(0, positionToOffset(content, { line: lineNum, column: offset }))
.replace(REGEX_CODE_SPAN, '')
.trim();

return /`[^`]*$/gm.test(textBefore);
};
Expand Down Expand Up @@ -51,10 +97,16 @@ export const mathEnvCheck = (doc: TextDocument, pos: Position): string => {
}
};

const sizeLimit = 50000; // ~50 KB
const fileSizesCache: { [path: string]: [number, boolean] } = {};
let fileSizesCache: { [path: string]: [number, boolean] } = {};

export const cleanFileSizesCache = () => {
fileSizesCache = {};
};

export const isFileTooLarge = (document: TextDocument): boolean => {
export const isFileTooLarge = (
document: TextDocument,
sizeLimit: number = 50000 /* ~50 KB */,
): boolean => {
const filePath = document.uri.fsPath;
if (!filePath || !fs.existsSync(filePath)) {
return false;
Expand Down

0 comments on commit 232d3a9

Please sign in to comment.