diff --git a/README.md b/README.md index e98e89cf..aef3f25d 100644 --- a/README.md +++ b/README.md @@ -639,7 +639,8 @@ The `file` is resolved relative to the current working directory. If an `extends key is present once read, its value will be resolved as a path relative to `file` and loaded recursively. Settings from a file referenced by `extends` are applied first, then those of `file` are applied on top (overriding any of the same keys -appearing in the referenced file). +appearing in the referenced file). If either the `file` or `extends` path begins +with the `~` directory, it will act as a placeholder for the home directory. #### parsers diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index 124f252e..066d9f18 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -995,6 +995,18 @@ function deepFreeze(obj) { return obj; } module.exports.deepFreeze = deepFreeze; +/** + * Expands a path with a tilde to an absolute path. + * + * @param {string} file Path that may begin with a tilde. + * @param {Object} os Node.js "os" module. + * @returns {string} Absolute path (or original path). + */ +function expandTildePath(file, os) { + var homedir = os && os.homedir(); + return homedir ? file.replace(/^~($|\/|\\)/, "".concat(homedir, "$1")) : file; +} +module.exports.expandTildePath = expandTildePath; /***/ }), @@ -1030,6 +1042,16 @@ module.exports = markdownit; /***/ }), +/***/ "?a32b": +/*!********************!*\ + !*** os (ignored) ***! + \********************/ +/***/ (() => { + +/* (ignored) */ + +/***/ }), + /***/ "?b85c": /*!**********************!*\ !*** path (ignored) ***! @@ -2070,6 +2092,8 @@ function readConfig(file, parsers, fs, callback) { fs = __webpack_require__(/*! fs */ "?ec0a"); } // Read file + var os = __webpack_require__(/*! os */ "?a32b"); + file = helpers.expandTildePath(file, os); fs.readFile(file, "utf8", function (err, content) { if (err) { return callback(err); @@ -2084,7 +2108,7 @@ function readConfig(file, parsers, fs, callback) { var configExtends = config.extends; if (configExtends) { delete config.extends; - return resolveConfigExtends(file, configExtends, fs, function (_, resolvedExtends) { return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) { + return resolveConfigExtends(file, helpers.expandTildePath(configExtends, os), fs, function (_, resolvedExtends) { return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) { if (errr) { return callback(errr); } @@ -2121,6 +2145,8 @@ function readConfigSync(file, parsers, fs) { fs = __webpack_require__(/*! fs */ "?ec0a"); } // Read file + var os = __webpack_require__(/*! os */ "?a32b"); + file = helpers.expandTildePath(file, os); var content = fs.readFileSync(file, "utf8"); // Try to parse file var _a = parseConfiguration(file, content, parsers), config = _a.config, message = _a.message; @@ -2131,7 +2157,7 @@ function readConfigSync(file, parsers, fs) { var configExtends = config.extends; if (configExtends) { delete config.extends; - var resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs); + var resolvedExtends = resolveConfigExtendsSync(file, helpers.expandTildePath(configExtends, os), fs); return __assign(__assign({}, readConfigSync(resolvedExtends, parsers, fs)), config); } return config; diff --git a/helpers/helpers.js b/helpers/helpers.js index 07683116..249830dd 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -1021,3 +1021,16 @@ function deepFreeze(obj) { return obj; } module.exports.deepFreeze = deepFreeze; + +/** + * Expands a path with a tilde to an absolute path. + * + * @param {string} file Path that may begin with a tilde. + * @param {Object} os Node.js "os" module. + * @returns {string} Absolute path (or original path). + */ +function expandTildePath(file, os) { + const homedir = os && os.homedir(); + return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file; +} +module.exports.expandTildePath = expandTildePath; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 224e1f42..15976d0e 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -1026,6 +1026,8 @@ function readConfig(file, parsers, fs, callback) { fs = require("fs"); } // Read file + const os = require("os"); + file = helpers.expandTildePath(file, os); fs.readFile(file, "utf8", (err, content) => { if (err) { return callback(err); @@ -1042,7 +1044,7 @@ function readConfig(file, parsers, fs, callback) { delete config.extends; return resolveConfigExtends( file, - configExtends, + helpers.expandTildePath(configExtends, os), fs, (_, resolvedExtends) => readConfig( resolvedExtends, @@ -1093,6 +1095,8 @@ function readConfigSync(file, parsers, fs) { fs = require("fs"); } // Read file + const os = require("os"); + file = helpers.expandTildePath(file, os); const content = fs.readFileSync(file, "utf8"); // Try to parse file const { config, message } = parseConfiguration(file, content, parsers); @@ -1103,7 +1107,11 @@ function readConfigSync(file, parsers, fs) { const configExtends = config.extends; if (configExtends) { delete config.extends; - const resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs); + const resolvedExtends = resolveConfigExtendsSync( + file, + helpers.expandTildePath(configExtends, os), + fs + ); return { ...readConfigSync(resolvedExtends, parsers, fs), ...config diff --git a/test/markdownlint-test-config.js b/test/markdownlint-test-config.js index 6c88ed5a..8a7de80b 100644 --- a/test/markdownlint-test-config.js +++ b/test/markdownlint-test-config.js @@ -2,10 +2,13 @@ "use strict"; +const os = require("os"); const path = require("path"); const test = require("ava").default; const markdownlint = require("../lib/markdownlint"); +const sameFileSystem = (path.relative(os.homedir(), __dirname) !== __dirname); + test.cb("configSingle", (t) => { t.plan(2); markdownlint.readConfig("./test/config/config-child.json", @@ -28,6 +31,20 @@ test.cb("configAbsolute", (t) => { }); }); +if (sameFileSystem) { + test.cb("configTilde", (t) => { + t.plan(2); + markdownlint.readConfig( + `~/${path.relative(os.homedir(), "./test/config/config-child.json")}`, + function callback(err, actual) { + t.falsy(err); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); + }); +} + test.cb("configMultiple", (t) => { t.plan(2); markdownlint.readConfig("./test/config/config-grandparent.json", @@ -60,9 +77,10 @@ test.cb("configMultipleWithRequireResolve", (t) => { }); test.cb("configCustomFileSystem", (t) => { - t.plan(5); - const file = path.resolve("/dir/file.json"); - const extended = path.resolve("/dir/extended.json"); + t.plan(3); + const file = "/dir/file.json"; + const extended = "~/dir/extended.json"; + const expanded = path.join(os.homedir(), extended.slice(1)); const fileContent = { "extends": extended, "default": true, @@ -74,20 +92,16 @@ test.cb("configCustomFileSystem", (t) => { }; const fsApi = { "access": (p, m, cb) => { - t.is(p, extended); + t.is(p, expanded); return (cb || m)(); }, "readFile": (p, o, cb) => { - switch (p) { - case file: - t.is(p, file); - return cb(null, JSON.stringify(fileContent)); - case extended: - t.is(p, extended); - return cb(null, JSON.stringify(extendedContent)); - default: - return t.fail(); + if (p === file) { + return cb(null, JSON.stringify(fileContent)); + } else if (p === expanded) { + return cb(null, JSON.stringify(extendedContent)); } + return t.fail(p); } }; markdownlint.readConfig( @@ -254,6 +268,16 @@ test("configAbsoluteSync", (t) => { t.deepEqual(actual, expected, "Config object not correct."); }); +if (sameFileSystem) { + test("configTildeSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync( + `~/${path.relative(os.homedir(), "./test/config/config-child.json")}`); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + }); +} + test("configMultipleSync", (t) => { t.plan(1); const actual = @@ -362,9 +386,10 @@ test("configMultipleHybridSync", (t) => { }); test("configCustomFileSystemSync", (t) => { - t.plan(4); - const file = path.resolve("/dir/file.json"); - const extended = path.resolve("/dir/extended.json"); + t.plan(2); + const file = "/dir/file.json"; + const extended = "~/dir/extended.json"; + const expanded = path.join(os.homedir(), extended.slice(1)); const fileContent = { "extends": extended, "default": true, @@ -376,19 +401,15 @@ test("configCustomFileSystemSync", (t) => { }; const fsApi = { "accessSync": (p) => { - t.is(p, extended); + t.is(p, expanded); }, "readFileSync": (p) => { - switch (p) { - case file: - t.is(p, file); - return JSON.stringify(fileContent); - case extended: - t.is(p, extended); - return JSON.stringify(extendedContent); - default: - return t.fail(); + if (p === file) { + return JSON.stringify(fileContent); + } else if (p === expanded) { + return JSON.stringify(extendedContent); } + return t.fail(p); } }; const actual = markdownlint.readConfigSync(file, null, fsApi); diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js index 8bad7e65..4d7c70a0 100644 --- a/test/markdownlint-test-helpers.js +++ b/test/markdownlint-test-helpers.js @@ -2,6 +2,8 @@ "use strict"; +const os = require("os"); +const path = require("path"); const test = require("ava").default; const helpers = require("../helpers"); @@ -1303,3 +1305,33 @@ test("htmlElementRanges", (t) => { const actual = helpers.htmlElementRanges(params, lineMetadata); t.deepEqual(actual, expected); }); + +test("expandTildePath", (t) => { + t.plan(16); + const homedir = os.homedir(); + t.is(helpers.expandTildePath("", os), ""); + t.is(helpers.expandTildePath("", null), ""); + t.is( + path.resolve(helpers.expandTildePath("~", os)), + homedir + ); + t.is(helpers.expandTildePath("~", null), "~"); + t.is(helpers.expandTildePath("file", os), "file"); + t.is(helpers.expandTildePath("file", null), "file"); + t.is(helpers.expandTildePath("/file", os), "/file"); + t.is(helpers.expandTildePath("/file", null), "/file"); + t.is( + path.resolve(helpers.expandTildePath("~/file", os)), + path.join(homedir, "/file") + ); + t.is(helpers.expandTildePath("~/file", null), "~/file"); + t.is(helpers.expandTildePath("dir/file", os), "dir/file"); + t.is(helpers.expandTildePath("dir/file", null), "dir/file"); + t.is(helpers.expandTildePath("/dir/file", os), "/dir/file"); + t.is(helpers.expandTildePath("/dir/file", null), "/dir/file"); + t.is( + path.resolve(helpers.expandTildePath("~/dir/file", os)), + path.join(homedir, "/dir/file") + ); + t.is(helpers.expandTildePath("~/dir/file", null), "~/dir/file"); +});