Permalink
Fetching contributors…
Cannot retrieve contributors at this time
460 lines (407 sloc) 14.7 KB

Advanced: Paragraph rule

Readme

You have already read following document.

This tutorial describe that creating a rule to handle Paragraph nodes.

Using Code Examples

You can see finished module in the tutorial.

The rule that specify the maximum word count of a sentence.

Using Library

These library are used in the module.

Terms

1. Start

To create a rule that specify the maximum word count of a sentence.

// Default options
const defaultOptions = {
    // max count of words >
    max: 50
};
/**
 * @param {TextLintRuleContext} context
 * @param {Object} options
 */
export default function (context, options = {}) {
    const {Syntax, getSource, RuleError, report} = context;
    const maxWordCount = options.max ? options.max : defaultOptions.max;
    return {
        [Syntax.Paragraph](node){
            // node is Paragraph node
            // Paragraph contain `Code`, `Str`, `Strong` node etc...
        }
    };
}

2. Ignore Code node

Paragraph node contain Code, Str, Strong node etc...

This is text.

is described as Paragraph.children = ["Str"]

ℹ️ Info:

But, Following Paragraph node contain Code node.

This text contain `var a = "string";` code.

How do you handle Code node?

  1. Ignore Code.
  2. Replace Code to dummy object that is a single word.

In this case, We select Case 2.

Replace Code to dummy object that is a single word Using unist-util-map.

// Helper for creating new AST using map function
// https://github.com/azu/unist-util-map
// if you want to filter, use https://github.com/eush77/unist-util-filter
import map from "unist-util-map";
import ObjectAssign from "object-assign";
// Default options
const defaultOptions = {
    // max count of words >
    max: 50
};
/**
 * @param {TextLintRuleContext} context
 * @param {Object} options
 */
export default function (context, options = {}) {
    const {Syntax, getSource, RuleError, report} = context;
    const maxWordCount = options.max ? options.max : defaultOptions.max;
    return {
        [Syntax.Paragraph](node){
            // replace code with dummy code
            // if you want to filter(remove) code, use https://github.com/eush77/unist-util-filter
            const filteredNode = map(node, (node) => {
                if (node.type === Syntax.Code) {
                    // only change `value` to dummy
                    return ObjectAssign({}, node, {
                        value: "code"
                    });
                }
                return node;
            });
        }
    };
}

3. Get text of a Paragraph

We can get text from a node with Source Map using textlint-util-to-string.

Why does we use textlint-util-to-string?

Because, we report error via context.report with original position of the text.

// Helper for creating new AST using map function
// https://github.com/azu/unist-util-map
// if you want to filter, use https://github.com/eush77/unist-util-filter
import map from "unist-util-map";
// Helper for converting plain text from Syntax-ed text(markdown AST
// https://github.com/azu/textlint-util-to-string
import StringSource from "textlint-util-to-string";
import ObjectAssign from "object-assign";
// Default options
const defaultOptions = {
    // max count of words >
    max: 50
};
/**
 * @param {TextLintRuleContext} context
 * @param {Object} options
 */
export default function (context, options = {}) {
    const {Syntax, getSource, RuleError, report} = context;
    const maxWordCount = options.max ? options.max : defaultOptions.max;
    return {
        [Syntax.Paragraph](node){
            // replace code with dummy code
            // if you want to filter(remove) code, use https://github.com/eush77/unist-util-filter
            const filteredNode = map(node, (node) => {
                if (node.type === Syntax.Code) {
                    // only change `value` to dummy
                    return ObjectAssign({}, node, {
                        value: "code"
                    });
                }
                return node;
            });
            // create StringSource 
            const source = new StringSource(filteredNode);
            // text in a paragraph
            const text = source.toString();
        }
    };
}

4. Create Sentences from Paragraph

sentence-splitter split text to Sentences.

// Helper for creating new AST using map function
// https://github.com/azu/unist-util-map
// if you want to filter, use https://github.com/eush77/unist-util-filter
import map from "unist-util-map";
// Helper for converting plain text from Syntax-ed text(markdown AST
// https://github.com/azu/textlint-util-to-string
import StringSource from "textlint-util-to-string";
// Helper for splitting text to sentences
// https://github.com/azu/sentence-splitter
import {split as splitSentence, Syntax as SplitterSyntax} from "sentence-splitter";
import ObjectAssign from "object-assign";
// Default options
const defaultOptions = {
    // max count of words >
    max: 50
};
/**
 * @param {TextLintRuleContext} context
 * @param {Object} options
 */
export default function (context, options = {}) {
    const {Syntax, getSource, RuleError, report} = context;
    const maxWordCount = options.max ? options.max : defaultOptions.max;
    return {
        [Syntax.Paragraph](node){
            // replace code with dummy code
            // if you want to filter(remove) code, use https://github.com/eush77/unist-util-filter
            const filteredNode = map(node, (node) => {
                if (node.type === Syntax.Code) {
                    // only change `value` to dummy
                    return ObjectAssign({}, node, {
                        value: "code"
                    });
                }
                return node;
            });
            const source = new StringSource(filteredNode);
            // text in a paragraph
            const text = source.toString();
            // get sentences from Paragraph
            const sentences = splitSentence(text).filter(node => {
                // ignore break line
                return node.type === SplitterSyntax.Sentence;
            });
            // text in a sentence
            sentences.forEach(sentence => {
                /* sentence object is a node
                {
                    type: "Sentence",
                    raw: text,
                    value: text,
                    loc: loc,
                    range: range
                };
                 */
                const sentenceText = sentence.value;
            });
        }
    };
}

4. Get words in a sentence

Split a Sentence to Words using split-string-words.

// Helper for creating new AST using map function
// https://github.com/azu/unist-util-map
// if you want to filter, use https://github.com/eush77/unist-util-filter
import map from "unist-util-map";
// Helper for converting plain text from Syntax-ed text(markdown AST
// https://github.com/azu/textlint-util-to-string
import StringSource from "textlint-util-to-string";
// Helper for splitting text to sentences
// https://github.com/azu/sentence-splitter
import {split as splitSentence, Syntax as SplitterSyntax} from "sentence-splitter";
// Helper for splitting text to words
// https://github.com/timjrobinson/split-string-words
import splitWord from "split-string-words";
import ObjectAssign from "object-assign";
// Default options
const defaultOptions = {
    // max count of words >
    max: 50
};
/**
 * @param {TextLintRuleContext} context
 * @param {Object} options
 */
export default function (context, options = {}) {
    const {Syntax, getSource, RuleError, report} = context;
    const maxWordCount = options.max ? options.max : defaultOptions.max;
    return {
        [Syntax.Paragraph](node){
            // replace code with dummy code
            // if you want to filter(remove) code, use https://github.com/eush77/unist-util-filter
            const filteredNode = map(node, (node) => {
                if (node.type === Syntax.Code) {
                    // only change `value` to dummy
                    return ObjectAssign({}, node, {
                        value: "code"
                    });
                }
                return node;
            });
            const source = new StringSource(filteredNode);
            // text in a paragraph
            const text = source.toString();
            // get sentences from Paragraph
            const sentences = splitSentence(text).filter(node => {
                // ignore break line
                return node.type === SplitterSyntax.Sentence;
            });
            // text in a sentence
            sentences.forEach(sentence => {
                /* sentence object is a node
                {
                    type: "Sentence",
                    raw: text,
                    value: text,
                    loc: loc,
                    range: range
                };
                 */
                const sentenceText = sentence.value;
                // words in a sentence
                const words = splitWord(sentenceText);
                // over count of word, then report error
                if (words.length > maxWordCount) {
                    // get original index value of sentence.loc.start
                    const originalIndex = source.originalIndexFromPosition(sentence.loc.start);
                    const ruleError = new RuleError(`Exceeds the maximum word count of ${maxWordCount}.`, {
                        index: originalIndex
                    });
                    report(node, ruleError);
                }
            });
        }
    };
}

If the count of word is over max, report this as RuleError.

// over count of word, then report error
if (words.length > maxWordCount) {
    // get original index value of sentence.loc.start
    const originalIndex = source.originalIndexFromPosition(sentence.loc.start);
    const ruleError = new RuleError(`Exceeds the maximum word count of ${maxWordCount}.`, {
        index: originalIndex
    });
    report(node, ruleError);
}

You can get original index from position of Sentence node.

const originalIndex = source.originalIndexFromPosition(sentence.loc.start);

Testing

Test the rule with textlint-tester.

const TextLintTester = require("textlint-tester");
const tester = new TextLintTester();
// rule
import rule from "../src/textlint-rule-en-max-word-count";
// ruleName, rule, { valid, invalid }
tester.run("max-word-count", rule, {
    valid: [
        // no match
        {
            text: "This is pen.",
            options: {
                max: 3
            }
        },
        // replace Code block to a word
        {
            text: "This is `code is a word`.",
            options: {
                max: 3
            }
        }
    ],
    invalid: [
        // single match
        {
            text: "This is a pen.",
            options: {
                max: 3
            },
            errors: [
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 1,
                    column: 1
                }
            ]
        },
        // multiple match in multiple lines
        {
            text: `This is a pen.

This is not a pen.`,
            options: {
                max: 3
            },
            errors: [
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 1,
                    column: 1
                },
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 3,
                    column: 1
                }
            ]
        },
        // multiple hit items in a line
        {
            text: "This is a pen.This is not a pen.",
            options: {
                max: 3
            },
            errors: [
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 1,
                    column: 1
                },
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 1,
                    column: 15
                }
            ]
        },
        // It is a single sentence
        {
            text: "This is a pen This is not a pen.",
            options: {
                max: 3
            },
            errors: [
                {
                    message: "Exceeds the maximum word count of 3.",
                    line: 1,
                    column: 1
                }
            ]
        }
    ]
});

Complete!

See completed source code in textlint-rule-en-max-word-count