summaryrefslogtreecommitdiff
path: root/tools/eslint/node_modules/eslint-plugin-markdown/lib/processor.js
blob: 7d0fa62f795914567b551b3a95670777c9e2fcde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
 * @fileoverview Processes Markdown files for consumption by ESLint.
 * @author Brandon Mills
 */

"use strict";

var assign = require("object-assign");
var parse5 = require("parse5");
var remark = require("remark");

var SUPPORTED_SYNTAXES = ["js", "javascript", "node", "jsx"];
var UNSATISFIABLE_RULES = [
    "eol-last" // The Markdown parser strips trailing newlines in code fences
];

var blocks = [];

/**
 * Performs a depth-first traversal of the Markdown AST.
 * @param {ASTNode} node A Markdown AST node.
 * @param {object} callbacks A map of node types to callbacks.
 * @param {object} [parent] The node's parent AST node.
 * @returns {void}
 */
function traverse(node, callbacks, parent) {
    var i;

    if (callbacks[node.type]) {
        callbacks[node.type](node, parent);
    }

    if (typeof node.children !== "undefined") {
        for (i = 0; i < node.children.length; i++) {
            traverse(node.children[i], callbacks, node);
        }
    }
}

/**
 * Converts leading HTML comments to JS block comments.
 * @param {string} html The text content of an HTML AST node.
 * @returns {string[]} An array of JS block comments.
 */
function getComments(html) {
    var ast = parse5.parse(html, { locationInfo: true });
    var nodes = ast.childNodes.filter(function(node) {
        return node.__location; // eslint-disable-line no-underscore-dangle
    });
    var comments = [];
    var index;

    for (index = nodes.length - 1; index >= 0; index--) {
        if (
            nodes[index].nodeName === "#comment"
            && nodes[index].data.trim().slice(0, "eslint".length) === "eslint"
        ) {
            comments.unshift("/*" + nodes[index].data + "*/");
        } else {
            break;
        }
    }

    return comments;
}

/**
 * Extracts lintable JavaScript code blocks from Markdown text.
 * @param {string} text The text of the file.
 * @returns {string[]} Source code strings to lint.
 */
function preprocess(text) {
    var ast = remark().parse(text);

    blocks = [];
    traverse(ast, {
        "code": function(node, parent) {
            var comments = [];
            var index, previousNode;

            if (node.lang && SUPPORTED_SYNTAXES.indexOf(node.lang.toLowerCase()) >= 0) {
                index = parent.children.indexOf(node);
                previousNode = parent.children[index - 1];
                if (previousNode && previousNode.type === "html") {
                    comments = getComments(previousNode.value) || [];
                }

                blocks.push(assign({}, node, { comments: comments }));
            }
        }
    });

    return blocks.map(function(block) {
        return block.comments.concat(block.value).join("\n");
    });
}

/**
 * Creates a map function that adjusts messages in a code block.
 * @param {Block} block A code block.
 * @returns {function} A function that adjusts messages in a code block.
 */
function adjustBlock(block) {
    var leadingCommentLines = block.comments.reduce(function(count, comment) {
        return count + comment.split("\n").length;
    }, 0);

    /**
     * Adjusts ESLint messages to point to the correct location in the Markdown.
     * @param {Message} message A message from ESLint.
     * @returns {Message} The same message, but adjusted ot the correct location.
     */
    return function adjustMessage(message) {
        var lineInCode = message.line - leadingCommentLines;
        if (lineInCode < 1) {
            return null;
        }

        return assign({}, message, {
            line: lineInCode + block.position.start.line,
            column: message.column + block.position.indent[lineInCode - 1] - 1
        });
    };
}

/**
 * Excludes unsatisfiable rules from the list of messages.
 * @param {Message} message A message from the linter.
 * @returns {boolean} True if the message should be included in output.
 */
function excludeUnsatisfiableRules(message) {
    return message && UNSATISFIABLE_RULES.indexOf(message.ruleId) < 0;
}

/**
 * Transforms generated messages for output.
 * @param {Array<Message[]>} messages An array containing one array of messages
 *     for each code block returned from `preprocess`.
 * @returns {Message[]} A flattened array of messages with mapped locations.
 */
function postprocess(messages) {
    return [].concat.apply([], messages.map(function(group, i) {
        var adjust = adjustBlock(blocks[i]);
        return group.map(adjust).filter(excludeUnsatisfiableRules);
    }));
}

module.exports = {
    preprocess: preprocess,
    postprocess: postprocess
};