editor icon indicating copy to clipboard operation
editor copied to clipboard

Isn't it possible to enable & disable specific preset by merge-config?

Open r7kamura opened this issue 2 years ago • 8 comments

As background, I am trying to provide a mechanism to bundle multiple rule presets into one Chrome extension and let users choose which one they want to use.

I tried to implement this using merge-config command while reading the documentation on the following page,

  • https://github.com/textlint/editor/tree/master/packages/%40textlint/script-compiler

like this:

textlintWorker.postMessage({
  command: "merge-config",
  textlintrc: {
    rules: {
      "preset-ja-technical-writing": true,
      "preset-japanese": false
    },
  },
});

but the above code did not work as intended to toggle disabling/enabling of the entire preset.

Is this currently not feasible without adding individual settings to switch disable/enable for each preset for each rule, as shown below?

textlintWorker.postMessage({
  command: "merge-config",
  textlintrc: {
    rules: {
      "preset-ja-technical-writing": {
        "max-comma": {
          max: Number.MAX_SAFE_INTEGER,
        },
        "sentence-length": {
          max: Number.MAX_SAFE_INTEGER,
        },
        "max-ten": {
          max: Number.MAX_SAFE_INTEGER,
        },
        "max-kanji-continuous-len": {
          max: Number.MAX_SAFE_INTEGER,
          allow: [],
        },
        "arabic-kanji-numbers": false,
        // ...
      },
      "preset-japanese": {
        // ...
      },
    },
  },
});

In this case, though, it makes no sense to use presets in the first place, and it would be easier to use the individual rules without presets.


In addition, I would like to take this opportunity to ask a follow-up question: isn't it possible to "disable a specific rule" in merge-config? I think that just mergeing false for each individual ruleId element won't work. By this issue, the above example also includes a workaround that sets a suitably large value for max.

r7kamura avatar Jan 03 '24 04:01 r7kamura

Probably, It's a bug

In textlint kernal, presets are expanded as separate rules and then loaded. I think the current implementation is failing to override the merge-config command because it has the wrong rule id to load.

https://github.com/textlint/editor/blob/d5d5a875ed3fc4a720d056363c7f8bb61937ecf4/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts#L110-L126

maybe like this.


diff --git a/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts b/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts
index d9d3732..462ca07 100644
--- a/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts
+++ b/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts
@@ -6,6 +6,7 @@ import type {
 } from "@textlint/config-loader";
 import type { TextlintFixResult, TextlintResult } from "@textlint/types";
 import { TextlintScriptMetadata } from "@textlint/script-parser";
+import { normalizeTextlintPresetSubRuleKey } from "@textlint/utils";
 
 export type MessageId = string;
 export type TextlintWorkerCommandLint = {
@@ -108,11 +109,19 @@ ${config.plugins.map(createPluginImportCode).join("\n")}
 const kernel = new TextlintKernel();
 const rules = ${stringify(
         config.rules.map((rule, index) => {
-            return {
-                ruleId: rule.ruleId,
-                rule: `!__moduleInterop(__rule${index})__!`,
-                options: rule.options
-            };
+            if (rule.type === "RuleInPreset") {
+                return {
+                    ruleId: normalizeTextlintPresetSubRuleKey({ preset: rule.ruleId, rule: rule.ruleKey }),
+                    rule: `!__moduleInterop(__rule${index})__!`,
+                    options: rule.options
+                };
+            } else {
+                return {
+                    ruleId: rule.ruleId,
+                    rule: `!__moduleInterop(__rule${index})__!`,
+                    options: rule.options
+                };
+            }
         })
     )};
 const filterRules = ${stringify(

merge-config already use {preset id}/{child rule id}

https://github.com/textlint/editor/blob/d5d5a875ed3fc4a720d056363c7f8bb61937ecf4/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts#L152-L172 https://github.com/textlint/editor/blob/e7f734eb2e1e44d85592159f37d3a83ab50f7966/packages/@textlint/config-partial-parser/src/parser.ts#L55-L85

azu avatar Jan 03 '24 05:01 azu

Oh, I misread that.

In textlint kernal, presets are expanded as separate rules and then loaded.

in script-compiler https://github.com/textlint/editor/blob/d5d5a875ed3fc4a720d056363c7f8bb61937ecf4/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts#L152-L172 in textlint https://github.com/textlint/textlint/blob/e184d1f33340bb3fc0bc13c50f62d8feb6ecae8a/packages/%40textlint/config-loader/src/loader.ts#L215-L223

Probably, We need to treat preset-abc: false as preset-abc/a: false, preset-abc/b: false .... in merge-config command as special case. https://github.com/textlint/editor/blob/d5d5a875ed3fc4a720d056363c7f8bb61937ecf4/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts#L152-L172

azu avatar Jan 03 '24 05:01 azu

In addition, I would like to take this opportunity to ask a follow-up question: isn't it possible to "disable a specific rule" in merge-config?

It is possible that the process of excluding rules with an option of false from the config may be missing. The "merge-config" has not been tested and should be re-created.

azu avatar Jan 03 '24 05:01 azu

This is a rough image, but I think we will need the following logic to merge configs.

export const assignConfig = (currentConfig: {
    plugins: TextlintKernelPlugin[];
    rules: TextlintKernelRule[];
    filterRules: TextlintKernelFilterRule[];
}, newConfig: TextlintRcConfig) => {
    const { rules, plugins, filterRules } = parseOptionsFromConfig(newConfig);
    const mergeItemsOptionById = (currentItems: TextlintStaticOptionRule[], newItems: TextlintStaticOptionRule[]) => {
        return newItems.flatMap((newItem) => {
            const currentItem = currentItems.find((item) => item.ruleId === newItem.ruleId);
            if (currentItem) {
                return {
                    ...currentItem,
                    options: newItem.options
                };
            }
            return newItem;
        })
    };
    const mergePluginById = (currentItems: TextlintStaticOptionPlugin[], newItems: TextlintStaticOptionPlugin[]) => {
        return newItems.flatMap((newItem) => {
            const currentItem = currentItems.find((item) => item.pluginId === newItem.pluginId);
            if (currentItem) {
                return {
                    ...currentItem,
                    options: newItem.options
                };
            }
            return newItem;
        })
    }
    // if options is false, this will be disabled
    return {
        rules: mergeItemsOptionById(currentConfig.rules, rules),
        filterRules: mergeItemsOptionById(currentConfig.filterRules, filterRules),
        plugins: mergePluginById(currentConfig.plugins, plugins)
    };
}

It is easier to test if implemented as a runtime library like @textlint/config-partial-parser rather than embedding it directly.

azu avatar Jan 03 '24 05:01 azu

I see. I like the idea of expanding { presetA: false } that way.

Reviewing from the design of config, I see there are two options on how to handle { ruleA: false } here.

One is to leave the current config as it is in its current design and regard false as a special instruction to delete the element when merge is performed, as you described.

The other is to design the config so that false is a special setting that disables the rule, so that when you merge, you do nothing special and false is the setting value of the rule. This way, I think it will work well not only when merge, but also when writing { ruleA: false } in .textlintrc, so there are fewer surprises and more symmetry.

r7kamura avatar Jan 06 '24 03:01 r7kamura

https://github.com/textlint/editor/issues/87#issuecomment-1874879652 This comment is probably wrong and hide it.

  • In textlint:
    • @textlint/config-loader expands preset to rules before passing config to kernel
    • ℹ️ This limitation come from that kernel can not use Node.js API like node:fs
  • In editor script:
    • Already expanded rules exists
    • editor script can not list rules from preset name unless pass more info

azu avatar Jan 07 '24 03:01 azu

The other is to design the config so that false is a special setting that disables the rule, so that when you merge, you do nothing special and false is the setting value of the rule. This way, I think it will work well not only when merge, but also when writing { ruleA: false } in .textlintrc, so there are fewer surprises and more symmetry.

Can you explain the detail? I couldn't imagine it imho.

azu avatar Jan 08 '24 01:01 azu

The other is to design the config so that false is a special setting that disables the rule, so that when you merge, you do nothing special and false is the setting value of the rule. This way, I think it will work well not only when merge, but also when writing { ruleA: false } in .textlintrc, so there are fewer surprises and more symmetry.

Can you explain the detail? I couldn't imagine it imho.

const config1 = {
  rules: {
    a: { /* ... */ }
  }
};

const config2 = {
  rules: {
    a: false
  }
};

const result = mergeConfig(config1, config2)

In the code above, I'm talking about whether the result should be { rules: {} } or { rules: { a: false } }. In the latter case, the responsibility for handling the false would be of the config value handler, not the merge handler.

r7kamura avatar Jan 08 '24 08:01 r7kamura