sommark 2.3.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +47 -42
  2. package/SOMMARK-SPEC.md +483 -0
  3. package/cli/cli.mjs +42 -2
  4. package/cli/commands/color.js +36 -0
  5. package/cli/commands/help.js +7 -0
  6. package/cli/commands/init.js +2 -0
  7. package/cli/commands/list.js +119 -0
  8. package/cli/commands/print.js +61 -11
  9. package/cli/commands/show.js +24 -27
  10. package/cli/constants.js +1 -1
  11. package/cli/helpers/config.js +14 -4
  12. package/cli/helpers/transpile.js +27 -32
  13. package/constants/html_props.js +100 -0
  14. package/constants/html_tags.js +146 -0
  15. package/constants/void_elements.js +26 -0
  16. package/core/lexer.js +70 -39
  17. package/core/parser.js +100 -84
  18. package/core/pluginManager.js +139 -0
  19. package/core/plugins/comment-remover.js +47 -0
  20. package/core/plugins/module-system.js +137 -0
  21. package/core/plugins/quote-escaper.js +37 -0
  22. package/core/plugins/raw-content-plugin.js +72 -0
  23. package/core/plugins/rules-validation-plugin.js +197 -0
  24. package/core/plugins/sommark-format.js +211 -0
  25. package/core/transpiler.js +65 -198
  26. package/debug.js +9 -4
  27. package/format.js +23 -0
  28. package/formatter/mark.js +3 -3
  29. package/formatter/tag.js +6 -2
  30. package/grammar.ebnf +5 -5
  31. package/helpers/camelize.js +2 -0
  32. package/helpers/colorize.js +20 -14
  33. package/helpers/kebabize.js +2 -0
  34. package/helpers/utils.js +161 -0
  35. package/index.js +243 -44
  36. package/mappers/languages/html.js +200 -105
  37. package/mappers/languages/json.js +23 -4
  38. package/mappers/languages/markdown.js +88 -67
  39. package/mappers/languages/mdx.js +130 -2
  40. package/mappers/mapper.js +77 -246
  41. package/package.json +7 -5
  42. package/unformatted.smark +90 -0
  43. package/v3-todo.smark +75 -0
  44. package/CHANGELOG.md +0 -119
  45. package/helpers/loadCss.js +0 -46
@@ -15,6 +15,11 @@ export function getHelp(unknown_option = true) {
15
15
  "{N} <$green:-v, --version$> <$cyan: Show version information$>",
16
16
  "{N} <$green:init$> <$cyan: Initialize global SomMark configuration directory$>",
17
17
  "{N} <$green:show config$> <$cyan: Display the absolute paths to the active SomMark configuration files$>",
18
+ "{N} <$green:color on|off$> <$cyan: Enable or disable colored output$>",
19
+ "{N} <$green:list plugins$> <$cyan: List all enabled plugins with author and description$>",
20
+ "{N} <$green:list -i plugins$> <$cyan: List only internal (built-in) plugins$>",
21
+ "{N} <$green:list -e plugins$> <$cyan: List only external (user-defined) plugins$>",
22
+ "{N} <$green:list pipeline$> <$cyan: Show the execution order of all active plugin phases$>",
18
23
 
19
24
  "{N}{N}<$yellow:Transpilation Options:$>",
20
25
  "{N}<$yellow:Usage:$> <$blue:sommark [option] [targetFile] [option] [outputFile] [outputDir]$>",
@@ -23,6 +28,8 @@ export function getHelp(unknown_option = true) {
23
28
  "{N} <$green:--mdx$> <$cyan: Transpile to MDX$>",
24
29
  "{N} <$green:--json$> <$cyan: Transpile to json$>",
25
30
  "{N} <$green:--text$> <$cyan: Transpile to plain text$>",
31
+ "{N} <$green:--lex$> <$cyan: Print lexer tokens to console$>",
32
+ "{N} <$green:--parse$> <$cyan: Print parser AST to console$>",
26
33
 
27
34
  "{N}{N}<$yellow:Output Options:$>",
28
35
  "{N} <$green:-p, --print$> <$cyan: Print output to console (stdout)$>",
@@ -38,6 +38,8 @@ export async function runInit() {
38
38
  outputDir: "",
39
39
  outputFile: "output",
40
40
  mappingFile: "",
41
+ plugins: [],
42
+ priority: [],
41
43
  };
42
44
  `;
43
45
 
@@ -0,0 +1,119 @@
1
+ import SomMark, { BUILT_IN_PLUGINS } from "../../index.js";
2
+ import { loadConfig } from "../helpers/config.js";
3
+ import { formatMessage } from "../../core/errors.js";
4
+
5
+ /**
6
+ * List Plugins Command
7
+ * smark list plugins
8
+ * smark list --internal plugins
9
+ * smark list --external plugins
10
+ */
11
+ export async function runListPlugins(args) {
12
+ const config = await loadConfig();
13
+ const enabledPlugins = config.plugins || [];
14
+
15
+ // Filter flags
16
+ const showInternal = args.includes("--internal") || args.includes("-i");
17
+ const showExternal = args.includes("--external") || args.includes("-e");
18
+ const showAll = (!showInternal && !showExternal) || (args.length === 1 && args[0] === "plugins");
19
+
20
+ console.log(formatMessage(`{N}<$yellow:SomMark Enabled Plugins:$>{N}`));
21
+
22
+ let found = false;
23
+
24
+ // 1. Internal Plugins
25
+ if (showInternal || showAll) {
26
+ const internal = enabledPlugins.filter(p => {
27
+ const name = typeof p === "string" ? p : p.name;
28
+ return BUILT_IN_PLUGINS.some(bp => bp.name === name);
29
+ });
30
+
31
+ if (internal.length > 0) {
32
+ console.log(formatMessage(` <$magenta:Internal Plugins (Built-in):$>{N}`));
33
+ internal.forEach(p => {
34
+ const name = typeof p === "string" ? p : p.name;
35
+ const pluginObj = BUILT_IN_PLUGINS.find(bp => bp.name === name);
36
+ printPluginInfo(name, pluginObj);
37
+ });
38
+ found = true;
39
+ }
40
+ }
41
+
42
+ // 2. External Plugins
43
+ if (showExternal || showAll) {
44
+ const external = enabledPlugins.filter(p => {
45
+ const name = typeof p === "string" ? p : p.name;
46
+ return !BUILT_IN_PLUGINS.some(bp => bp.name === name);
47
+ });
48
+
49
+ if (external.length > 0) {
50
+ console.log(formatMessage(`${found ? "{N}" : ""} <$magenta:External Plugins (User-defined):$>{N}`));
51
+ external.forEach(p => {
52
+ const name = typeof p === "string" ? p : (p.name || "Unknown");
53
+ printPluginInfo(name, typeof p === "object" ? p : null);
54
+ });
55
+ found = true;
56
+ }
57
+ }
58
+
59
+ if (!found) {
60
+ console.log(formatMessage(` <$red:No plugins enabled or matching the filter.$>{N}`));
61
+ } else {
62
+ console.log("");
63
+ }
64
+ }
65
+
66
+ /**
67
+ * List Pipeline Command
68
+ * smark list pipeline
69
+ */
70
+ export async function runListPipeline() {
71
+ const config = await loadConfig();
72
+ const sm = new SomMark({
73
+ src: "",
74
+ format: "html", // Default format
75
+ plugins: config.plugins,
76
+ priority: config.priority
77
+ });
78
+
79
+ const pipeline = sm.pluginManager.plugins;
80
+
81
+ console.log(formatMessage(`{N}<$yellow:SomMark Execution Pipeline:$>{N}`));
82
+
83
+ const phases = [
84
+ { name: "1. Preprocessors (Global)", type: "preprocessor", scope: "top-level" },
85
+ { name: "2. Preprocessors (Scoped)", type: "preprocessor", scope: "arguments" },
86
+ { name: "3. After Lexer (Tokens)", type: ["lexer", "after-lexer"] },
87
+ { name: "4. AST Handlers (Parser Hooks)", type: ["parser", "on-ast"] },
88
+ { name: "5. Mapper Extensions (Rules)", type: "mapper" },
89
+ { name: "6. Output Transformers (Final)", type: ["transform", "postprocessor"] }
90
+ ];
91
+
92
+ phases.forEach((phase, index) => {
93
+ const types = Array.isArray(phase.type) ? phase.type : [phase.type];
94
+ const matched = pipeline.filter(p => {
95
+ const pTypes = Array.isArray(p.type) ? p.type : [p.type];
96
+ const typeMatch = pTypes.some(t => types.includes(t));
97
+ const scopeMatch = !phase.scope || p.scope === phase.scope;
98
+ return typeMatch && scopeMatch;
99
+ });
100
+
101
+ console.log(formatMessage(` <$magenta:${phase.name}$>`));
102
+ if (matched.length > 0) {
103
+ matched.forEach(p => {
104
+ console.log(formatMessage(` <$green:• ${p.name}$> <$cyan:[${Array.isArray(p.type) ? p.type.join(", ") : p.type}]$>`));
105
+ });
106
+ } else {
107
+ console.log(formatMessage(` <$yellow: (None registered)$>`));
108
+ }
109
+ if (index < phases.length - 1) console.log("");
110
+ });
111
+ console.log("");
112
+ }
113
+
114
+ function printPluginInfo(name, pluginObj) {
115
+ const author = pluginObj?.author || "Unknown";
116
+ const desc = pluginObj?.description || "No description provided.";
117
+ console.log(formatMessage(` <$green:• ${name}$> <$cyan:by ${author}$>`));
118
+ console.log(formatMessage(` <$yellow:${desc}$>`));
119
+ }
@@ -1,23 +1,73 @@
1
1
  import { cliError, formatMessage } from "../../core/errors.js";
2
2
  import { isExist, readContent } from "../helpers/file.js";
3
3
  import { transpile } from "../helpers/transpile.js";
4
+ import SomMark from "../../index.js";
5
+ import { loadConfig } from "../helpers/config.js";
4
6
  import path from "node:path";
5
7
 
6
8
  // ========================================================================== //
7
9
  // Print Output //
8
10
  // ========================================================================== //
9
11
  export async function printOutput(format, filePath) {
10
- if (await isExist(filePath)) {
11
- const fileName = path.basename(filePath);
12
- console.log(formatMessage(`{line}<$blue: Printing output for$> <$yellow:'${fileName}'$>{line}`));
13
- let source_code = await readContent(filePath);
14
- if (format ==="json") {
15
- const output = await transpile({ src: source_code.toString(), format });
16
- console.log(JSON.stringify(JSON.parse(output, null, 2), null, 2));
17
- } else {
18
- console.log(await transpile({ src: source_code.toString(), format }));
19
- }
12
+ if (await isExist(filePath)) {
13
+ const fileName = path.basename(filePath);
14
+ console.log(formatMessage(`{line}<$blue: Printing output for$> <$yellow:'${fileName}'$>{line}`));
15
+ let source_code = await readContent(filePath);
16
+ if (format === "json") {
17
+ const output = await transpile({ src: source_code.toString(), format });
18
+ console.log(JSON.stringify(JSON.parse(output, null, 2), null, 2));
20
19
  } else {
21
- cliError([`{line}<$red:File$> <$blue:'${filePath}'$> <$red: is not found$>{line}`]);
20
+ console.log(await transpile({ src: source_code.toString(), format }));
22
21
  }
22
+ } else {
23
+ cliError([`{line}<$red:File$> <$blue:'${filePath}'$> <$red: is not found$>{line}`]);
24
+ }
25
+ }
26
+
27
+ // ========================================================================== //
28
+ // Print Lexer Tokens //
29
+ // ========================================================================== //
30
+ export async function printLex(filePath) {
31
+ if (await isExist(filePath)) {
32
+ const fileName = path.basename(filePath);
33
+ console.log(formatMessage(`{line}<$blue: Printing tokens for$> <$yellow:'${fileName}'$>{line}`));
34
+ const source_code = await readContent(filePath);
35
+ const config = await loadConfig();
36
+
37
+ const smark = new SomMark({
38
+ src: source_code.toString(),
39
+ format: "text",
40
+ plugins: config.plugins,
41
+ priority: config.priority
42
+ });
43
+
44
+ const tokens = await smark.lex();
45
+ console.log(JSON.stringify(tokens, null, 2));
46
+ } else {
47
+ cliError([`{line}<$red:File$> <$blue:'${filePath}'$> <$red: is not found$>{line}`]);
48
+ }
49
+ }
50
+
51
+ // ========================================================================== //
52
+ // Print Parser AST //
53
+ // ========================================================================== //
54
+ export async function printParse(filePath) {
55
+ if (await isExist(filePath)) {
56
+ const fileName = path.basename(filePath);
57
+ console.log(formatMessage(`{line}<$blue: Printing AST for$> <$yellow:'${fileName}'$>{line}`));
58
+ const source_code = await readContent(filePath);
59
+ const config = await loadConfig();
60
+
61
+ const smark = new SomMark({
62
+ src: source_code.toString(),
63
+ format: "text",
64
+ plugins: config.plugins,
65
+ priority: config.priority
66
+ });
67
+
68
+ const ast = await smark.parse();
69
+ console.log(JSON.stringify(ast, null, 2));
70
+ } else {
71
+ cliError([`{line}<$red:File$> <$blue:'${filePath}'$> <$red: is not found$>{line}`]);
72
+ }
23
73
  }
@@ -1,46 +1,43 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
1
  import { cliError, formatMessage } from "../../core/errors.js";
4
- import { getConfigDir } from "./init.js";
2
+ import { loadConfig, getResolvedConfigPath } from "../helpers/config.js";
5
3
 
6
4
  // ========================================================================== //
7
5
  // Show Command //
8
6
  // ========================================================================== //
9
7
 
10
8
  export async function runShow(target) {
9
+ const config = await loadConfig();
10
+ const resolvedPath = getResolvedConfigPath();
11
+
11
12
  if (target === "config") {
12
13
  try {
13
- const configDir = getConfigDir();
14
- const configFilePath = path.join(configDir, "smark.config.js");
15
- const pluginsDir = path.join(configDir, "plugins");
16
- //========================================================================== //
17
- // Helper to check existence and format message //
18
- //========================================================================== //
19
- const checkPath = async (itemPath, isDir = false) => {
20
- try {
21
- await fs.access(itemPath);
22
- return `<$green:${itemPath}$> <$cyan:(Exists)$>`;
23
- } catch {
24
- return `<$red:${itemPath}$> <$yellow:(Not Found)$>`;
25
- }
26
- };
27
-
28
- const configStatus = await checkPath(configFilePath);
29
- const pluginsStatus = await checkPath(pluginsDir, true);
30
-
31
- console.log(formatMessage(`{N}<$yellow:SomMark Configuration Files:$>{N}`));
32
- console.log(formatMessage(` <$magenta:Config File:$> ${configStatus}`));
33
- console.log(formatMessage(` <$magenta:Plugins Dir:$> ${pluginsStatus}{N}`));
34
-
14
+ console.log(formatMessage(`{N}<$yellow:SomMark Configuration Data:$>{N}`));
15
+
16
+ // Format config for display (hide large objects)
17
+ const displayConfig = { ...config };
18
+ if (displayConfig.mappingFile && typeof displayConfig.mappingFile === "object") {
19
+ displayConfig.mappingFile = "[Mapper Object]";
20
+ }
21
+
22
+ console.log(JSON.stringify(displayConfig, null, 4));
23
+ console.log("");
35
24
  } catch (error) {
36
25
  cliError([
37
- `{line}<$red:Failed to retrieve configuration paths:$> <$magenta:${error.message}$>{line}`
26
+ `{line}<$red:Failed to retrieve configuration data:$> <$magenta:${error.message}$>{line}`
38
27
  ]);
39
28
  }
29
+ } else if (target === "--path-config") {
30
+ console.log(formatMessage(`{N}<$yellow:SomMark Configuration Path:$>{N}`));
31
+ if (resolvedPath) {
32
+ console.log(formatMessage(` <$green:${resolvedPath}$>{N}`));
33
+ } else {
34
+ console.log(formatMessage(` <$red:No configuration file found. Using defaults.$>{N}`));
35
+ }
40
36
  } else {
41
37
  cliError([
42
38
  `{line}<$red:Invalid target for 'show' command:$> <$blue:'${target || ""}'$> `,
43
- `<$yellow:Usage:$> <$cyan:sommark show config$>{line}`
39
+ `{N}<$yellow:Usage:$> {N} <$cyan:sommark show config$> - Displays configuration data`,
40
+ ` <$cyan:sommark show --path-config$> - Displays configuration file path{line}`
44
41
  ]);
45
42
  }
46
43
  }
package/cli/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // CLI Constants //
3
3
  // ========================================================================== //
4
4
 
5
- export const options = ["-v", "--version", "-h", "--help", "--html", "--markdown", "--mdx", "--json", "--text", "--print", "-p"];
5
+ export const options = ["-v", "--version", "-h", "--help", "--html", "--markdown", "--mdx", "--json", "--text", "--print", "-p", "--lex", "--parse", "list"];
6
6
 
7
7
  export const extensions = {
8
8
  text: "txt",
@@ -17,22 +17,28 @@ const localConfigPath = path.join(currentDir, CONFIG_FILE_NAME);
17
17
  let config = {
18
18
  outputFile: "output",
19
19
  outputDir: "",
20
- mappingFile: ""
20
+ mappingFile: "",
21
+ plugins: [],
22
+ priority: []
21
23
  };
22
24
 
23
25
  // ========================================================================== //
24
26
  // Load Configuration //
25
27
  // ========================================================================== //
28
+ let resolvedConfigPath = null;
29
+
26
30
  export async function loadConfig() {
27
31
  const userConfigPath = path.join(getConfigDir(), CONFIG_FILE_NAME);
28
32
  let targetConfigPath = null;
29
33
 
30
- if (await isExist(userConfigPath)) {
31
- targetConfigPath = userConfigPath;
32
- } else if (await isExist(localConfigPath)) {
34
+ if (await isExist(localConfigPath)) {
33
35
  targetConfigPath = localConfigPath;
36
+ } else if (await isExist(userConfigPath)) {
37
+ targetConfigPath = userConfigPath;
34
38
  }
35
39
 
40
+ resolvedConfigPath = targetConfigPath;
41
+
36
42
  if (targetConfigPath) {
37
43
  try {
38
44
  const configURL = pathToFileURL(targetConfigPath).href;
@@ -50,3 +56,7 @@ export async function loadConfig() {
50
56
  }
51
57
  return config;
52
58
  }
59
+
60
+ export function getResolvedConfigPath() {
61
+ return resolvedConfigPath;
62
+ }
@@ -1,53 +1,48 @@
1
1
  import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { cliError } from "../../core/errors.js";
4
- import lexer from "../../core/lexer.js";
5
- import parser from "../../core/parser.js";
6
- import transpiler from "../../core/transpiler.js";
4
+ import SomMark from "../../index.js";
7
5
  import HTML from "../../mappers/languages/html.js";
8
6
  import MARKDOWN from "../../mappers/languages/markdown.js";
9
7
  import MDX from "../../mappers/languages/mdx.js";
10
8
  import Json from "../../mappers/languages/json.js";
11
9
  import { isExist } from "./file.js";
12
10
  import { loadConfig } from "./config.js";
13
- import { htmlFormat, markdownFormat, mdxFormat, jsonFormat } from "../../core/formats.js";
11
+ import { htmlFormat, markdownFormat, mdxFormat, jsonFormat, textFormat } from "../../core/formats.js";
14
12
 
15
- const default_mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json};
13
+ const default_mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [textFormat]: null };
16
14
 
17
15
  // ========================================================================== //
18
16
  // Transpile Function //
19
17
  // ========================================================================== //
20
18
  export async function transpile({ src, format, mappingFile = "" }) {
21
- if (typeof mappingFile === "object" && mappingFile !== null) {
22
- return await transpiler({ ast: parser(lexer(src)), format, mapperFile: mappingFile });
23
- }
24
-
25
19
  const config = await loadConfig();
20
+ let finalMapper = mappingFile;
26
21
 
27
- // Use config mapping file if not provided as argument
28
- if (config.mappingFile) {
29
- mappingFile = config.mappingFile;
30
- } else {
31
- mappingFile = default_mapperFiles[format];
32
- }
22
+ // 1. Resolve Mapping File
23
+ if (typeof mappingFile !== "object" || mappingFile === null) {
24
+ if (config.mappingFile) {
25
+ finalMapper = config.mappingFile;
26
+ } else {
27
+ finalMapper = default_mapperFiles[format];
28
+ }
33
29
 
34
- // Check if mappingFile is an object (loaded from config)
35
- if (typeof mappingFile === "object" && mappingFile !== null) {
36
- return await transpiler({ ast: parser(lexer(src)), format, mapperFile: mappingFile });
30
+ // Custom Mapper (String Path)
31
+ if (typeof finalMapper === "string" && finalMapper !== "" && (await isExist(finalMapper))) {
32
+ const mappingFileURL = pathToFileURL(path.resolve(process.cwd(), finalMapper)).href;
33
+ const loadedMapper = await import(mappingFileURL);
34
+ finalMapper = loadedMapper.default;
35
+ }
37
36
  }
38
37
 
39
- // ========================================================================== //
40
- // Custom Mapper (String Path) //
41
- // ========================================================================== //
42
- else if (typeof mappingFile === "string" && (await isExist(mappingFile))) {
43
- const mappingFileURL = pathToFileURL(path.resolve(process.cwd(), mappingFile)).href;
44
- const loadedMapper = await import(mappingFileURL);
45
- return await transpiler({ ast: parser(lexer(src)), format, mapperFile: loadedMapper.default });
46
- }
47
- // ========================================================================== //
48
- // Error: Mapper not found //
49
- // ========================================================================== //
50
- else {
51
- cliError([`{line}<$red:File$> <$blue:'${mappingFile}'$> <$red: is not found$>{line}`]);
52
- }
38
+ // 2. Use SomMark Unified API
39
+ const smark = new SomMark({
40
+ src,
41
+ format,
42
+ mapperFile: finalMapper,
43
+ plugins: config.plugins,
44
+ priority: config.priority
45
+ });
46
+
47
+ return await smark.transpile();
53
48
  }
@@ -0,0 +1,100 @@
1
+ export const HTML_PROPS = new Set([
2
+ // Global Attributes
3
+ "id",
4
+ "class",
5
+ "className",
6
+ "title",
7
+ "lang",
8
+ "dir",
9
+ "tabindex",
10
+ "hidden",
11
+ "accesskey",
12
+ "draggable",
13
+ "spellcheck",
14
+ "contenteditable",
15
+ "role",
16
+ "style",
17
+ "slot",
18
+ "autofocus",
19
+ "translate",
20
+ "enterkeyhint",
21
+ "inputmode",
22
+
23
+ // Common Element-Specific
24
+ "href",
25
+ "src",
26
+ "alt",
27
+ "type",
28
+ "name",
29
+ "value",
30
+ "placeholder",
31
+ "target",
32
+ "rel",
33
+ "width",
34
+ "height",
35
+ "loading",
36
+ "decoding",
37
+ "crossorigin",
38
+ "charset",
39
+ "action",
40
+ "method",
41
+ "enctype",
42
+ "autocomplete",
43
+ "required",
44
+ "readonly",
45
+ "disabled",
46
+ "multiple",
47
+ "pattern",
48
+ "min",
49
+ "max",
50
+ "step",
51
+ "checked",
52
+ "selected",
53
+ "rows",
54
+ "cols",
55
+ "wrap",
56
+ "for",
57
+ "media",
58
+ "async",
59
+ "defer",
60
+ "poster",
61
+ "controls",
62
+ "autoplay",
63
+ "loop",
64
+ "muted",
65
+ "preload",
66
+ "sandbox",
67
+ "allow",
68
+ "allowfullscreen",
69
+ "anchor",
70
+ "autocapitalize",
71
+ "autocorrect",
72
+ "capture",
73
+ "download",
74
+ "exportparts",
75
+ "form",
76
+ "formaction",
77
+ "formenctype",
78
+ "formmethod",
79
+ "formnovalidate",
80
+ "formtarget",
81
+ "inert",
82
+ "integrity",
83
+ "is",
84
+ "itemid",
85
+ "itemprop",
86
+ "itemref",
87
+ "itemscope",
88
+ "itemtype",
89
+ "list",
90
+ "maxlength",
91
+ "minlength",
92
+ "nonce",
93
+ "part",
94
+ "playsinline",
95
+ "popover",
96
+ "referrerpolicy",
97
+ "sizes",
98
+ "srcset",
99
+ "virtualkeyboardpolicy"
100
+ ]);