Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 44 additions & 22 deletions src/Sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const {
writeEnd,
} = require("./common");
const { writeFile, unlink } = require("fs");
const dedent = require("dedent");
const path = require("path");
const arrayBuffToBuff = require("arraybuffer-to-buffer");
const anzip = require("anzip");
Expand All @@ -25,6 +24,9 @@ const progress = require("cli-progress");
const glob = require("glob");
const { type } = require("os");

// Deindenting dependencies
const { deindentByCommonPrefix, SENSITIVE_INDENT_EXTS } = require("./deindent");

// Convert dependency functions to return promises
const writeAsync = promisify(writeFile);
const unlinkAsync = promisify(unlink);
Expand Down Expand Up @@ -56,21 +58,37 @@ class Snippet {
lines.push(textline);
}
if (config.select !== undefined) {
const selectedLines = selectLines(config.select, this.lines, this.ext);
lines.push(...selectedLines);
let snippetLines = selectLines(config.select, this.lines, this.ext);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we assemble the lines of the snippet and where I run the lines through the dedenting functions


if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) {
snippetLines = deindentByCommonPrefix(snippetLines);
}

lines.push(...snippetLines);
} else if(!config.startPattern && !config.endPattern ) {
lines.push(...this.lines);
let snippetLines = [...this.lines];

if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) {
snippetLines = deindentByCommonPrefix(snippetLines);
}

lines.push(...snippetLines);
} else {
// use the patterns to grab the content specified.

const pattern = new RegExp(`(${config.startPattern}[\\s\\S]+${config.endPattern})`);
const match = this.lines.join("\n").match(pattern);

if (match !== null) {
let filteredLines = match[1].split("\n");
lines.push(...filteredLines);
let snippetLines = match[1].split("\n");

if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) {
snippetLines = deindentByCommonPrefix(snippetLines);
}

lines.push(...snippetLines);
}
}
}

if (config.enable_code_block) {
lines.push(markdownCodeTicks);
Expand Down Expand Up @@ -136,13 +154,8 @@ class File {
this.lines = [];
}
// fileString converts the array of lines into a string
fileString(dedentCode = false) {
fileString() {
let lines = `${this.lines.join("\n")}\n`;

if (dedentCode) {
lines = dedent(lines);
}

return lines;
}
}
Expand Down Expand Up @@ -467,7 +480,7 @@ class Sync {
for (const file of files) {
await writeAsync(
file.fullpath,
file.fileString(this.config.features.enable_code_dedenting)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the old dedenting implementation to avoid confusing.

file.fileString()
);
this.progress.increment();
}
Expand Down Expand Up @@ -530,15 +543,24 @@ function extractWriteIDAndConfig(line) {
function overwriteConfig(current, extracted) {
let config = {};

config.enable_source_link =
extracted?.enable_source_link ?? true
? current.enable_source_link
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fancy looking but I thought it was too hard to read

: extracted.enable_source_link;
// use snippet override if present, otherwise use global default
if (extracted && 'enable_source_link' in extracted) {
config.enable_source_link = extracted.enable_source_link;
} else {
config.enable_source_link = current.enable_source_link;
}

if (extracted && 'enable_code_block' in extracted) {
config.enable_code_block = extracted.enable_code_block;
} else {
config.enable_code_block = current.enable_code_block;
}

config.enable_code_block =
extracted?.enable_code_block ?? true
? current.enable_code_block
: extracted.enable_code_block;
if (extracted && 'enable_code_dedenting' in extracted) {
config.enable_code_dedenting = extracted.enable_code_dedenting;
} else {
config.enable_code_dedenting = current.enable_code_dedenting || false;
}

if (extracted?.highlightedLines ?? undefined) {
config.highlights = extracted.highlightedLines;
Expand Down
2 changes: 1 addition & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports.readConfig = (logger, file="") => {

// Disable code block dedenting by default if not specified
if (!Object.prototype.hasOwnProperty.call(cfg.features, 'enable_code_dedenting')) {
cfg['features']['enable_code_dedenting'] = false;
cfg['features']['enable_code_dedenting'] = true;
}

return cfg;
Expand Down
37 changes: 37 additions & 0 deletions src/deindent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function commonIndentPrefix(lines) {
const nonEmpty = lines.filter(l => l.trim().length > 0);
if (nonEmpty.length === 0) {return "";}

// Treat lines that are only closing tokens (possibly multiple) as "closers".
// Examples matched: "}", ")", "]", "});", "],", "})", "));", etc., with optional spaces.
const CLOSING_ONLY = /^\s*[\])}]+(?:[;,])?\s*$/;
const RUBY_END = /^\s*end\b\s*$/;
const isClosingOnly = (s) => CLOSING_ONLY.test(s) || RUBY_END.test(s);

// Ignore closers when computing the common indent
const pool = nonEmpty.filter(l => !isClosingOnly(l.trim()));
const candidates = pool.length ? pool : nonEmpty;

const prefixes = candidates.map(l => (l.match(/^[\t ]*/)?.[0] || ""));
let prefix = prefixes[0] || "";
for (let i = 1; i < prefixes.length; i++) {
let j = 0;
while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) {j++;}
prefix = prefix.slice(0, j);
if (!prefix) {break;}
}
return prefix;
}

function deindentByCommonPrefix(lines) {
const prefix = commonIndentPrefix(lines);
if (!prefix) {
return lines.slice();
}
const re = new RegExp("^" + prefix.replace(/[\t ]/g, m => (m === "\t" ? "\\t" : " ")));
return lines.map(l => (l.startsWith(prefix) ? l.replace(re, "") : l));
}

const SENSITIVE_INDENT_EXTS = new Set(['make', 'mk', 'Makefile', 'diff']);

module.exports = { commonIndentPrefix, deindentByCommonPrefix, SENSITIVE_INDENT_EXTS };
94 changes: 94 additions & 0 deletions test/deindent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const { deindentByCommonPrefix, commonIndentPrefix } = require('../src/deindent');

describe('deindentByCommonPrefix', () => {
test('strips common leading spaces from all lines', () => {
const input = [
' foo();',
' bar();',
];
const output = deindentByCommonPrefix(input);
expect(output).toEqual(['foo();', 'bar();']);
});

test('preserves relative indentation inside the snippet', () => {
const input = [
' if (x) {',
' doSomething();',
' }',
];
const output = deindentByCommonPrefix(input);
expect(output).toEqual([
'if (x) {',
' doSomething();',
'}',
]);
});

test('does not dedent when common indent is zero (hanging indent case)', () => {
const input = [
' a++;',
'b = a;',
];
const output = deindentByCommonPrefix(input);
// Nothing stripped
expect(output).toEqual(input);
});

test('ignores closing-only head lines when computing indent', () => {
const input = [
'});',
' doSomething();',
];
const output = deindentByCommonPrefix(input);
expect(output).toEqual([
'});', // unchanged
'doSomething();', // dedented
]);
});

test('handles empty lines gracefully', () => {
const input = [
'',
' foo();',
'',
' bar();',
];
const output = deindentByCommonPrefix(input);
expect(output).toEqual([
'',
'foo();',
'',
'bar();',
]);
});

test('returns a shallow copy when there is nothing to dedent', () => {
const input = ['foo();', 'bar();'];
const output = deindentByCommonPrefix(input);
expect(output).toEqual(input);
expect(output).not.toBe(input); // new array, not the same reference
});
});

describe('commonIndentPrefix', () => {
test('computes the correct common indent', () => {
const input = [
' foo();',
' bar();',
];
expect(commonIndentPrefix(input)).toBe(' ');
});

test('returns empty string when lines have different starting indents and at least one substantive line is flush left', () => {
const input = [
' foo();',
'bar();',
];
expect(commonIndentPrefix(input)).toBe('');
});

test('returns empty string for all-empty input', () => {
expect(commonIndentPrefix([])).toBe('');
expect(commonIndentPrefix(['', ''])).toBe('');
});
});
8 changes: 8 additions & 0 deletions test/fixtures/dedent.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
What if there is preceding text at zero indentation? Does it handle this well?

<!--SNIPSTART typescript-ejson-worker -->
<!--SNIPEND-->

For example, this paragraph starts at flush-left.

- However, certain items should not be dedented
- For example, this list item on another level
- Another list item
Loading