markdownlint icon indicating copy to clipboard operation
markdownlint copied to clipboard

Integrate OnkarRuikar/markdownlint-rule-search-replace

Open nschonni opened this issue 1 year ago • 13 comments

As part of the Markdownlint cleanup on mdn/content, @OnkarRuikar made this interesting rule https://github.com/OnkarRuikar/markdownlint-rule-search-replace

This is a very useful swiss army knife rule that allows projects to identify (and optionally fix) issues specific to their code base.

One of the limitations that might be able to be addressed if move here, is that the entire ruleset needs to be disabled if you want to disable a single rule in a file. I believe that the limitation currently is that the disable syntax doesn't support extra metadata like mardownlint-disable search-replace:my-sub-rule1

Maybe if integrating it doesn't make sense, then I could spin up a separate issue around that metadata disable part

nschonni avatar Dec 08 '22 02:12 nschonni

That's really neat, I love it!! It's not a "normal" rule like the existing MD### rules, so my current thinking is to leave it separate. (I see it's already tagged so it will be easier to find.)

/cc @OnkarRuikar

DavidAnson avatar Dec 08 '22 03:12 DavidAnson

No problem, maybe they can explain the issue with disabling a nested custom rule better and we can repurpose this issue.

nschonni avatar Dec 08 '22 06:12 nschonni

I love it!! It's not a "normal" rule like the existing MD### rules, so my current thinking is to leave it separate.

If we look at this from other perspective, this feature eases the markdownlint custom rule creation; non-developers can define their rules easily by simply editing the config file. So this is not a custom rule but a custom rule creation feature. Implementing the feature directly in markdownlint will ensure those custom rules provide same functionality as normal MD### rules do. With this, markdownlint can become a framework for implementing and enforcing such rules in documentation projects. Every org/project has it's documentation dos and don'ts, which can be enforced with this feature.

If we want to keep it separate then can we mention it in markdownlint/README.md file? Because people may find coding custom rules difficult for various reasons.


One of the limitations that might be able to be addressed if move here, is that the entire ruleset needs to be disabled if you want to disable a single rule in a file. I believe that the limitation currently is that the disable syntax doesn't support extra metadata like mardownlint-disable search-replace:my-sub-rule1

For context behind this refer mdn/content#21432 (comment).

Following issues need to be addressed to support inline enable/disable comments(lets call it inline configurations):

  1. Inline configurations are not exposed to the custom rules. The params object in function rule(params, onError) doesn't have this info.
  2. All HTML comments are stripped before passing the content to custom rules.
    The custom rule sees <!--...............--> and not the actual inline configuration, so it can't implement the logic on it's own.

Re-implementing the entire feature in custom rules is not feasible in long term, we'll have to keep synchronizing the feature with the markdownlint repo. So we shouldn't address the issue no. 2 mentioned above. On the other side, Markdownlint won't know about the sub-rules so it can't enforce the inline configurations itself. Only way I see is that markdownlint provides the configurations and helper methods to custom rules.

I think following provisions from markdownlint repo are required: a. Decide on syntax for the markup which will work with existing stuff e.g.
<!-- markdownlint-disable search-replace(fqdn-moz-links, m-dash, curly-double-quotes) MD005 -->
This shouldn't disable the entire rule, but used for passing the info to the custom rule.
<!-- markdownlint-disable search-replace --> should continue disabling the entire rule.

b. Pass the configuration object to custom rules. More simplifications to the object are welcomed:

  • passing only the rule related info
  • breaking search-replace(fqdn-moz-links, m-dash) to search-replace:fqdn-moz-links and search-replace:m-dash configs

c. The helpers.js need to provide a function to convert the config object(b.) to ranges object like existing codeBlockAndSpanRanges function does.

OnkarRuikar avatar Dec 08 '22 10:12 OnkarRuikar

I am happy to include a link to this rule from the documentation! If you'd like to send a PR, I expect to publish a new release soon, and it could be part of that.

Regarding your suggestions around passing more context to rules when disabling them, you make good points. I don't want to rush into that, so I'm not going to make it part of this release, but I have added a note to myself to think about it for the following release.

Thanks again for the contribution, I really like what you've done to make custom rules more approachable!

DavidAnson avatar Dec 08 '22 17:12 DavidAnson

BTW, I realized afterwards, that this could also replace something like MD044 https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md044---proper-names-should-have-the-correct-capitalization

nschonni avatar Dec 08 '22 17:12 nschonni

@OnkarRuikar

The “selectively disable part of a rule” proposal feels weird and special-case to me. I think a more general approach might be to allow reconfiguring a rule inline, but that would be a significant change. Since yours is the only scenario I recall where this has come up so far, what about an alternative approach? You could re-export the markdownlint-rule-search-replace rule multiple times under different names and then configure them with the different sets of strings and disable the set you want by rule name in the usual manner. If it works, this should be possible today. To be clearer, I’m imagining something like this (from memory on my phone):

const rule = require(“markdownlint-rule-search-replace”);
module.exports = [
  {
    ...rule,
    “names”: [ “rule-one” ]
  },
  {
    ...rule,
    “names”: [ “rule-two” ]
  }
];

DavidAnson avatar Jul 14 '23 06:07 DavidAnson

The “selectively disable part of a rule” proposal feels weird and special-case to me.

In short, the custom rule only needs disabled line ranges and names of the disabled sub-rules.

what about an alternative approach? You could re-export the markdownlint-rule-search-replace rule multiple times under different names and then configure them with the different sets of strings and disable the set you want by rule name in the usual manner.

Are you suggesting to write a new custom rule in the target repo(say mdn/content) that will do the splitting or to write a wrapper around markdownlint-cli2 tool?

Is it possible to export multiple rule definitions from a single custom rule file, i.e. module.exports = [r1, r2, ...]? Do we have to split the configuration as well?

OnkarRuikar avatar Jul 14 '23 07:07 OnkarRuikar

I am proposing a new "meta-rule" which exports an array of multiple instances of the existing rule like you show (already supported today). Each name would need to be unique, but the implementation should all be the same, so the pattern I show should be all the extra "code" that's needed. If you specify configuration via JSON, it would need to be duplicated for each unique name. If you specify configuration via JS, you could reuse a common base object for each instance. You shouldn't have to do anything to the CLI and this should work with all the tools, I think.

DavidAnson avatar Jul 14 '23 16:07 DavidAnson

Regarding splitting configuration, it looks like you have about eight different definitions in the file you link to. In the extreme, you could give each one its own instance of the sub-rule, therefore exporting eight copies from the meta-rule. Or you could just break off one or two of those that need to be disabled inline so your meta-rule would only have two or three exports.

DavidAnson avatar Jul 14 '23 16:07 DavidAnson

Actually, the name "meta-rule" is a bad choice on my part. A single file can export multiple rules in an array. That's all I'm suggesting. Each one of those would have a new name but would otherwise be exactly the definition your rule has today.

DavidAnson avatar Jul 14 '23 16:07 DavidAnson

In the extreme, you could give each one its own instance of the sub-rule, therefore exporting eight copies from the meta-rule.

I think we don't have to split the sub-rules in the config file. We can simply, in the "meta-rule", intercept the sub-rule's function call and update params object to have only the required sub-rule configuration.

// search-replace-spllitter.js (aka meta-rule)

const rule = require(“markdownlint-rule-search-replace”);
const config = readConfig(".markdownlint-cli2.jsonc");

const subRules = [];

for (const subRuleConfig in config["search-replace"].rules) {
  const newRule = { 
    ...rule, 
    "names": [subRuleConfig.name]
  };
  const originalFunction = rule.function.bind(newRule);

  newRule.function = (params, onError) => {   // wrap the "function" definition 
    // and tweak the "params" to have only the required rule config
    params.config.rules = [subRuleConfig];

    return originalFunction(params, onError);
  };

  subRules.add(newRule);
}

module.exports = subRules;

This way the config file will remain the same, non weird, for the normal contributors.

Edit: updated the function binding location.

OnkarRuikar avatar Jul 14 '23 17:07 OnkarRuikar

Smart!!! (Just curious, why do you need to bind vs. calling rule.function with the modified params directly?

DavidAnson avatar Jul 14 '23 19:07 DavidAnson

Smart!!! (Just curious, why do you need to bind vs. calling rule.function with the modified params directly?

In case the custom rule uses this in future and to not to rely on original rule object to get the function reference.
I did the binding at the wrong place though. I've updated the code in the comment.

OnkarRuikar avatar Jul 15 '23 07:07 OnkarRuikar