From 0f62d6039f7c2cee2026b3b2aee94e537d0ee3f2 Mon Sep 17 00:00:00 2001 From: William Killerud Date: Sun, 7 Jul 2024 18:52:51 +0200 Subject: [PATCH] feat: support parsing indented syntax (with some minor limitations) (#13) In particular: code will always be the empty string, and the lines can not be calculated. --- README.md | 40 +- package-lock.json | 124 +++- package.json | 4 +- src/annotation.ts | 4 +- src/sass-comment-parser.ts | 254 ++++++++ src/sassdoc-parser-indented.test.ts | 563 ++++++++++++++++++ src/sassdoc-parser-two.fixture.scss | 2 - src/sassdoc-parser.fixture.scss | 2 - src/sassdoc-parser.ts | 53 +- src/sorter.ts | 2 +- ...ss-comment-parser.d.ts => cdocparser.d.ts} | 46 +- 11 files changed, 1007 insertions(+), 87 deletions(-) create mode 100644 src/sass-comment-parser.ts create mode 100644 src/sassdoc-parser-indented.test.ts delete mode 100644 src/sassdoc-parser-two.fixture.scss delete mode 100644 src/sassdoc-parser.fixture.scss rename types/{scss-comment-parser.d.ts => cdocparser.d.ts} (56%) diff --git a/README.md b/README.md index f4c9963..fd8d645 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # scss-sassdoc-parser -A more lightweight parser for SassDoc. - -More or less a thin wrapper around `scss-comment-parser`, but with all SassDoc annotations and TypeScript definitions built in. +A lightweight parser for SassDoc. ## Usage @@ -56,6 +54,40 @@ const singlePathResult = await doParse("_helpers.scss"); const arrayOfPathsResult = await doParse(["_mixins.scss", "_functions.scss"]); ``` +### Indented syntax + +The parser can handle indented syntax with a caveat: + +- The `context` field will not include accurate `code` or `line` fields. + +```js +import { parseSync } from "scss-sassdoc-parser"; + +const result = parseSync( + ` +/// Converts a value to the given unit +/// @param {Number} $value - Value to add unit to +/// @param {String} $unit - String representation of the unit +/// @return {Number} - $value expressed in $unit +@function to-length($value, $unit) + $units: ( + "px": 1px, + "rem": 1rem, + "%": 1%, + "em": 1em, + ) + + @if not index(map-keys($units), $unit) + $_: log("Invalid unit #{$unit}.") + + @return $value * map.get($units, $unit) +`, +); +``` + ## Output -The result from the `parse` function is an array of [`ParseResult` (type definitions)](/src/types.ts#L87). Check out the [snapshot tests](/src/sassdoc-parser.test.ts) for some example outputs. +The result from the `parse` function is an array of [`ParseResult` (type definitions)](/src/types.ts#L87). Check out the snapshot for some example outputs: + +- [Example output for SCSS](/src/sassdoc-parser.test.ts) +- [Example output for indented](/src/sassdoc-parser-indented.test.ts) diff --git a/package-lock.json b/package-lock.json index 7bac084..7c8c8fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "scss-comment-parser": "^0.8.4" + "cdocparser": "0.15.0" }, "devDependencies": { "@semantic-release/git": "^10.0.1", @@ -2192,12 +2192,14 @@ } }, "node_modules/cdocparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/cdocparser/-/cdocparser-0.13.0.tgz", - "integrity": "sha512-bMi4t0qjeT0xQ8ECBmWcilMYcUNYsERQoatXveMIbItgqliZDCNyv2xfkBoKrs5H08ApeRMoysJLwgPiHtv7HQ==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/cdocparser/-/cdocparser-0.15.0.tgz", + "integrity": "sha512-UvDuONjyQTyhYqnO1Glv6TLNi7GYQUq0uMyEYDMTc5QA/wlLmEUl00921pXNW+w0HjHXhoYKiJwwKknIzAcjxg==", + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2", "lodash.assign": "^2.4.1", + "lodash.union": "^3.1.0", "strip-indent": "^1.0.0" } }, @@ -2209,20 +2211,6 @@ "node": ">=0.8.0" } }, - "node_modules/cdocparser/node_modules/strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", - "dependencies": { - "get-stdin": "^4.0.1" - }, - "bin": { - "strip-indent": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -4663,6 +4651,48 @@ "lodash.isobject": "~2.4.1" } }, + "node_modules/lodash._baseflatten": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz", + "integrity": "sha512-fESngZd+X4k+GbTxdMutf8ohQa0s3sJEHIcwtu4/LsIQ2JTDzdRxDCMQjW+ezzwRitLmHnacVVmosCbxifefbw==", + "license": "MIT", + "dependencies": { + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/lodash._baseindexof": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz", + "integrity": "sha512-bSYo8Pc/f0qAkr8fPJydpJjtrHiSynYfYBjtANIgXv5xEf1WlTC63dIDlgu0s9dmTvzRu1+JJTxcIAHe+sH0FQ==", + "license": "MIT" + }, + "node_modules/lodash._baseuniq": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-3.0.3.tgz", + "integrity": "sha512-80ifiewpXTvE5gJ4+dnck+3ys4ix3+ch3N0/1ZvujIfbwIu0SnNIlJE4VsOS2bVjAcxm1JE8LLskpnQBgOR0bQ==", + "license": "MIT", + "dependencies": { + "lodash._baseindexof": "^3.0.0", + "lodash._cacheindexof": "^3.0.0", + "lodash._createcache": "^3.0.0" + } + }, + "node_modules/lodash._cacheindexof": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz", + "integrity": "sha512-S8dUjWr7SUT/X6TBIQ/OYoCHo1Stu1ZRy6uMUSKqzFnZp5G5RyQizSm6kvxD2Ewyy6AVfMg4AToeZzKfF99T5w==", + "license": "MIT" + }, + "node_modules/lodash._createcache": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash._createcache/-/lodash._createcache-3.1.2.tgz", + "integrity": "sha512-ev5SP+iFpZOugyab/DEUQxUeZP5qyciVTlgQ1f4Vlw7VUcCD8fVnyIqVUEIaoFH9zjAqdgi69KiofzvVmda/ZQ==", + "license": "MIT", + "dependencies": { + "lodash._getnative": "^3.0.0" + } + }, "node_modules/lodash._createwrapper": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-2.4.1.tgz", @@ -4674,6 +4704,12 @@ "lodash.isfunction": "~2.4.1" } }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "license": "MIT" + }, "node_modules/lodash._isnative": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", @@ -4742,6 +4778,18 @@ "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.4.1.tgz", "integrity": "sha512-VRYX+8XipeLjorag5bz3YBBRJ+5kj8hVBzfnaHgXPZAVTYowBdY5l0M5ZnOmlAMCOXBFabQtm7f5VqjMKEji0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "license": "MIT" + }, "node_modules/lodash.isfunction": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.4.1.tgz", @@ -4788,6 +4836,12 @@ "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.4.1.tgz", "integrity": "sha512-uNcV98/blRhInPUGQEnj9ekXXfG+q+rfoNSFZgl/eBfog9yBDW9gfUv2AHX/rAF7zZRlzWhbslGhbGQFZlCkZA==" }, + "node_modules/lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "license": "MIT" + }, "node_modules/lodash.support": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.4.1.tgz", @@ -4796,6 +4850,17 @@ "lodash._isnative": "~2.4.1" } }, + "node_modules/lodash.union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-3.1.0.tgz", + "integrity": "sha512-5W10Z523AFWxHAg+KuCynyv7U3L8qQdJL1yyKUXgpmDJYAoZIHl1KJBFqA5eHPcL9ObxHHUmALo8CUhptZQaKw==", + "license": "MIT", + "dependencies": { + "lodash._baseflatten": "^3.0.0", + "lodash._baseuniq": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, "node_modules/lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", @@ -9123,14 +9188,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/scss-comment-parser": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/scss-comment-parser/-/scss-comment-parser-0.8.4.tgz", - "integrity": "sha512-ERw4BODvM22n8Ke8hJxuH3fKXLm0Q4chfUNHwDSOAExCths2ZXq8PT32vms4R9Om6dffRSXzzGZS1p38UU4EAg==", - "dependencies": { - "cdocparser": "^0.13.0" - } - }, "node_modules/semantic-release": { "version": "22.0.8", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.8.tgz", @@ -9625,6 +9682,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", + "license": "MIT", + "dependencies": { + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 75e0c72..868d85b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "engines": { "node": ">=20" }, - "description": "A more lightweight parser for SassDoc in SCSS files, with TypeScript definitions.", + "description": "A more lightweight parser for SassDoc, with TypeScript definitions.", "keywords": [ "sassdoc", "scss", @@ -60,6 +60,6 @@ "vitest": "^1.0.0" }, "dependencies": { - "scss-comment-parser": "^0.8.4" + "cdocparser": "0.15.0" } } diff --git a/src/annotation.ts b/src/annotation.ts index ecd8732..70b8889 100644 --- a/src/annotation.ts +++ b/src/annotation.ts @@ -1,8 +1,8 @@ +import annotations from "./annotations/index.js"; import type { Annotation as ScssCommentParserAnnotation, ParseResult as ScssCommentParserParseResult, -} from "scss-comment-parser"; -import annotations from "./annotations/index.js"; +} from "./sass-comment-parser.js"; export type BuiltInAnnotationNames = | "access" diff --git a/src/sass-comment-parser.ts b/src/sass-comment-parser.ts new file mode 100644 index 0000000..db1059c --- /dev/null +++ b/src/sass-comment-parser.ts @@ -0,0 +1,254 @@ +import { + CommentExtractor, + CommentParser, + ParserConfig as CDocParserConfig, + Annotations, + Annotation, + Range, + Context, + ContextType, +} from "cdocparser"; +import { ParseResult } from "./types.js"; + +export type { + Annotation, + Annotations, + Range, + Context, + ContextType, + ParseResult, +}; + +export interface ParserConfig extends CDocParserConfig {} + +// Look for strings starting with @ (an at-rule like @mixin or @function) or % (placeholder selector) or $ (variable) followed by an identifier. +const contextRe = + /^(@|%|\$)([\w-_]+)*(?:\s+([\w-_]+)|[\s\S]*?:([\s\S]*?)(?:\s!(\w+))?;?[ \t]*?(?=\/\/|\n|$))?/; + +class Parser { + commentParser: CommentParser; + private extractor: CommentExtractor; + + constructor(annotations: Annotations, config?: ParserConfig) { + this.commentParser = new CommentParser(annotations, config); + this.extractor = new CommentExtractor(this.contextParser.bind(this), { + blockComment: false, + }); + } + + /** + * Extract the code following `offset` in `code` buffer, + * delimited by braces. + * + * `offset` should be set to the position of the opening brace. If not, + * the function will jump to the next opening brace. + * + * @param {String} code Code buffer. + * @param {Number} offset Index of the opening brace. + * @return {String} Extracted code between braces. + */ + private _extractCode(code: string, offset: number): string { + offset = offset || 0; + + if (code[offset] !== "{") { + // The position is not valid, jump to next opening brace + offset = code.indexOf("{", offset); + } + + const start = offset + 1; // Ignore the opening brace + let cursor = start; + let depth = 1; // The opening brace is consumed + const length = code.length; + + let inString = false; + let openChar = ""; + + // In block comment (line comments are instantly consumed) + let inComment = false; + + while (cursor < length && depth > 0) { + const cb = code[cursor - 1]; // Char before + const c = code[cursor]; // Char + const cn = code[cursor + 1]; // Char next + + if (!inString) { + if (c === "/" && cn === "/" && !inComment) { + // Swallow line comment + cursor = Math.min( + Math.max(code.indexOf("\r", cursor), code.indexOf("\n", cursor)), + length, + ); + continue; + } else if (c === "/" && cn === "*") { + // Block comment: begin + cursor += 2; // Swallow opening + inComment = true; + continue; + } else if (c === "*" && cn === "/") { + // Block comment: end + cursor += 2; // Swallow closing + inComment = false; + continue; + } + } + + if (!inComment) { + if ((c === '"' || c === "'") && cb !== "\\") { + if (!inString) { + // String: begin + openChar = c; + inString = true; + cursor++; + continue; + } else if (openChar === c) { + // String: end + inString = false; + cursor++; + continue; + } + } + } + + if (!(inString || inComment)) { + if (c === "{") { + depth++; + } else if (c === "}") { + depth--; + } + } + + cursor++; + } + + if (depth > 0) { + return ""; + } + + // Ignore the closing brace + cursor--; + + return code.substring(start, cursor); + } + + private contextParser( + ctxCode: string, + lineNumberFor: (index?: number) => number, + ): Context { + const match = contextRe.exec(ctxCode.trim()); + let startIndex, endIndex; + + const context: Context = { + type: "unknown", + } as Context; + + if (match) { + const wsOffset = Math.min(ctxCode.match(/\s*/)!.length - 1, 0); + startIndex = wsOffset + match.index; + endIndex = startIndex + match[0].length; + + if ( + match[1] === "@" && + (match[2] === "function" || match[2] === "mixin") + ) { + context.type = match[2]; + context.name = match[3]; + endIndex = this._addCodeToContext(context, ctxCode, match); + } else if (match[1] === "%") { + context.type = "placeholder"; + context.name = match[2]; + endIndex = this._addCodeToContext(context, ctxCode, match); + } else if (match[1] === "$") { + context.type = "variable"; + context.name = match[2]; + context.value = match[4].trim(); + context.scope = (match[5] || "private") as "private" | "global"; + } + } else { + startIndex = this._findCodeStart(ctxCode, 0); + endIndex = ctxCode.length - 1; + + if (startIndex > 0) { + context.type = "css"; + context.name = ctxCode.slice(0, startIndex).trim(); + context.value = this._extractCode(ctxCode, startIndex).trim(); + } + } + + if ( + lineNumberFor !== undefined && + startIndex !== undefined && + endIndex !== undefined + ) { + context.line = { + start: lineNumberFor(startIndex) + 1, + end: lineNumberFor(endIndex) + 1, + }; + } + + return context; + } + + private _findCodeStart(ctxCode: string, lastMatch?: number): number { + const codeStart = ctxCode.indexOf("{", lastMatch); + + if (codeStart < 0 || ctxCode[codeStart - 1] !== "#") { + return codeStart; + } + + return this._findCodeStart(ctxCode, codeStart + 1); + } + + private _addCodeToContext( + context: Context, + ctxCode: string, + match: RegExpMatchArray, + ): number | undefined { + const codeStart = this._findCodeStart(ctxCode, match.index); + + if (codeStart >= 0) { + context.code = this._extractCode(ctxCode, codeStart); + return codeStart + context.code!.length + 1; // Add closing brace! + } else { + context.code = ""; + } + + return undefined; + } + + parse(code: string, id?: string): ParseResult[] { + const comments = this.extractor.extract(code); + for (const comment of comments) { + comment.lines = this._filterAndGroup(comment.lines); + } + return this.commentParser.parse(comments, id) as ParseResult[]; + } + + private _filterAndGroup(lines: string[]): string[] { + const nLines: string[] = []; + let group = false; + + lines.forEach(function (line) { + const isAnnotation = line.indexOf("@") === 0; + + if (line.trim().indexOf("---") !== 0) { + // Ignore lines that start with "---" + if (group) { + if (isAnnotation) { + nLines.push(line); + } else { + nLines[nLines.length - 1] += "\n" + line; + } + } else if (isAnnotation) { + group = true; + nLines.push(line); + } else { + nLines.push(line); + } + } + }); + + return nLines; + } +} + +export default Parser; diff --git a/src/sassdoc-parser-indented.test.ts b/src/sassdoc-parser-indented.test.ts new file mode 100644 index 0000000..413684d --- /dev/null +++ b/src/sassdoc-parser-indented.test.ts @@ -0,0 +1,563 @@ +import { test, expect } from "vitest"; +import { parse, parseSync } from "./sassdoc-parser.js"; + +test("parses a decked out function", async () => { + const result = await parse(/* sass */ ` +/// Example trying to max out the number of annotations so we don't need so many test cases +/// @param {Number} $value - Value to add unit to +/// @param {String} $unit - String representation of the unit +/// @return {Number} - $value expressed in $unit +/// @deprecated Prefer other-item +/// @alias other-item +/// @see yet-another-item +/// @author Just Testing +/// @group helpers +/// @since 1.0.0 +/// @example Add unit +/// to-length($number, "%") +@function to-length($value, $unit) + $units: ( + "px": 1px, + "rem": 1rem, + "%": 1%, + "em": 1em, + ) + + @if not index(map-keys($units), $unit) + $_: log("Invalid unit #{$unit}.") + + @return $value * map.get($units, $unit) + +/// Other item +@function other-item($value) + @return $value + +/// Yet another item +@function yet-another-item($value) + @return $value +`); + + expect(result).toMatchInlineSnapshot(` + [ + { + "access": "public", + "alias": "other-item", + "author": [ + "Just Testing", + ], + "commentRange": { + "end": 13, + "start": 2, + }, + "context": { + "code": "", + "name": "to-length", + "type": "function", + }, + "deprecated": "Prefer other-item", + "description": "Example trying to max out the number of annotations so we don't need so many test cases + ", + "example": [ + { + "code": "to-length($number, "%")", + "description": "unit", + "type": "Add", + }, + ], + "group": [ + "helpers", + ], + "name": "to-length", + "parameter": [ + { + "description": "Value to add unit to", + "name": "value", + "type": "Number", + }, + { + "description": "String representation of the unit", + "name": "unit", + "type": "String", + }, + ], + "return": { + "description": "$value expressed in $unit", + "type": "Number", + }, + "see": [ + { + "access": "public", + "commentRange": { + "end": 31, + "start": 31, + }, + "context": { + "code": "", + "name": "yet-another-item", + "type": "function", + }, + "description": "Yet another item + ", + "group": [ + "undefined", + ], + "name": "yet-another-item", + }, + ], + "since": [ + { + "version": "1.0.0", + }, + ], + }, + { + "access": "public", + "aliased": [ + "to-length", + ], + "commentRange": { + "end": 27, + "start": 27, + }, + "context": { + "code": "", + "name": "other-item", + "type": "function", + }, + "description": "Other item + ", + "group": [ + "undefined", + ], + "name": "other-item", + }, + { + "access": "public", + "commentRange": { + "end": 31, + "start": 31, + }, + "context": { + "code": "", + "name": "yet-another-item", + "type": "function", + }, + "description": "Yet another item + ", + "group": [ + "undefined", + ], + "name": "yet-another-item", + }, + ] + `); +}); + +test("parses a decked out variable", async () => { + const result = await parse(/* sass */ ` +/// Example trying to max out the number of annotations so we don't need so many test cases +/// @access public +/// @deprecated Prefer valley +/// @alias stardew-alias +/// @see {variable} valley +/// @author Just Testing +/// @group tokens +/// @since 1.0.0 +/// @example +/// font-color: $stardew +$stardew: #ffffff + +/// @todo Document me +$stardew-alias: #fcfcfc + +/// @todo Document me +$valley: #000000 +`); + + expect(result).toMatchInlineSnapshot(` + [ + { + "access": "public", + "alias": "stardew-alias", + "author": [ + "Just Testing", + ], + "commentRange": { + "end": 11, + "start": 2, + }, + "context": { + "line": { + "end": 12, + "start": 12, + }, + "name": "stardew", + "scope": "private", + "type": "variable", + "value": "#ffffff", + }, + "deprecated": "Prefer valley", + "description": "Example trying to max out the number of annotations so we don't need so many test cases + ", + "example": [ + { + "code": "font-color: $stardew", + "type": "scss", + }, + ], + "group": [ + "tokens", + ], + "name": "stardew", + "see": [ + { + "access": "public", + "commentRange": { + "end": 17, + "start": 17, + }, + "context": { + "line": { + "end": 18, + "start": 18, + }, + "name": "valley", + "scope": "private", + "type": "variable", + "value": "#000000", + }, + "description": "", + "group": [ + "undefined", + ], + "name": "valley", + "todo": [ + "Document me", + ], + }, + ], + "since": [ + { + "version": "1.0.0", + }, + ], + }, + { + "access": "public", + "aliased": [ + "stardew", + ], + "commentRange": { + "end": 14, + "start": 14, + }, + "context": { + "line": { + "end": 15, + "start": 15, + }, + "name": "stardew-alias", + "scope": "private", + "type": "variable", + "value": "#fcfcfc", + }, + "description": "", + "group": [ + "undefined", + ], + "name": "stardew-alias", + "todo": [ + "Document me", + ], + }, + { + "access": "public", + "commentRange": { + "end": 17, + "start": 17, + }, + "context": { + "line": { + "end": 18, + "start": 18, + }, + "name": "valley", + "scope": "private", + "type": "variable", + "value": "#000000", + }, + "description": "", + "group": [ + "undefined", + ], + "name": "valley", + "todo": [ + "Document me", + ], + }, + ] + `); +}); + +test("parses a decked out mixin", async () => { + const result = await parse(/* sass */ ` +/// Keeps it secret +/// @output Sets display to hidden +@mixin _keep-it-secret + display: hidden + +/// Keeps it safe +/// @content Wraps in media query for print +@mixin _keep-it-safe + @media print + @content + +/// Where is the ring? +/// @param {String} $where [here] - Tell us where it is +@mixin _ring-is($where: "here") + content: $where +`); + + expect(result).toMatchInlineSnapshot(` + [ + { + "access": "private", + "commentRange": { + "end": 14, + "start": 13, + }, + "context": { + "code": "", + "name": "_ring-is", + "type": "mixin", + }, + "description": "Where is the ring? + ", + "group": [ + "undefined", + ], + "name": "_ring-is", + "parameter": [ + { + "default": "here", + "description": "Tell us where it is", + "name": "where", + "type": "String", + }, + ], + }, + { + "access": "private", + "commentRange": { + "end": 3, + "start": 2, + }, + "context": { + "code": "String", + "line": { + "end": 14, + "start": 4, + }, + "name": "_keep-it-secret", + "type": "mixin", + }, + "description": "Keeps it secret + ", + "group": [ + "undefined", + ], + "name": "_keep-it-secret", + "output": "Sets display to hidden", + }, + { + "access": "private", + "commentRange": { + "end": 8, + "start": 7, + }, + "content": "Wraps in media query for print", + "context": { + "code": "String", + "line": { + "end": 14, + "start": 9, + }, + "name": "_keep-it-safe", + "type": "mixin", + }, + "description": "Keeps it safe + ", + "group": [ + "undefined", + ], + "name": "_keep-it-safe", + }, + ] + `); +}); + +test("gives a default name that can be overridden with the @name annotation", async () => { + const result = await parse(/* sass */ ` +/// This is a test +$primary-color: #000000 + +/// This is a test +/// @name wants-to-be-the-primary-color +$secondary-color: #000000 +`); + expect(result[0].name).toEqual("primary-color"); + expect(result[1].name).toEqual("wants-to-be-the-primary-color"); + expect(result[1].context.name).toEqual("secondary-color"); +}); + +test("parseSync works", () => { + const result = parseSync(/* sass */ ` + /// Example trying to max out the number of annotations so we don't need so many test cases + /// @param {Number} $value - Value to add unit to + /// @param {String} $unit - String representation of the unit + /// @return {Number} - $value expressed in $unit + /// @deprecated Prefer other-item + /// @alias other-item + /// @see yet-another-item + /// @author Just Testing + /// @group helpers + /// @since 1.0.0 + /// @example Add unit + /// to-length($number, "%") + @function to-length($value, $unit) + $units: ( + "px": 1px, + "rem": 1rem, + "%": 1%, + "em": 1em, + ) + + @if not index(map-keys($units), $unit) + $_: log("Invalid unit #{$unit}.") + + @return $value * map.get($units, $unit) + + /// Other item + @function other-item($value) + @return $value + + /// Yet another item + @function yet-another-item($value) + @return $value + `); + + expect(result).toMatchInlineSnapshot(` + [ + { + "access": "public", + "alias": "other-item", + "author": [ + "Just Testing", + ], + "commentRange": { + "end": 13, + "start": 2, + }, + "context": { + "code": "", + "name": "to-length", + "type": "function", + }, + "deprecated": "Prefer other-item", + "description": "Example trying to max out the number of annotations so we don't need so many test cases + ", + "example": [ + { + "code": "to-length($number, "%")", + "description": "unit", + "type": "Add", + }, + ], + "group": [ + "helpers", + ], + "name": "to-length", + "parameter": [ + { + "description": "Value to add unit to", + "name": "value", + "type": "Number", + }, + { + "description": "String representation of the unit", + "name": "unit", + "type": "String", + }, + ], + "return": { + "description": "$value expressed in $unit", + "type": "Number", + }, + "see": [ + { + "access": "public", + "commentRange": { + "end": 31, + "start": 31, + }, + "context": { + "code": "", + "name": "yet-another-item", + "type": "function", + }, + "description": "Yet another item + ", + "group": [ + "undefined", + ], + "name": "yet-another-item", + }, + ], + "since": [ + { + "version": "1.0.0", + }, + ], + }, + { + "access": "public", + "aliased": [ + "to-length", + ], + "commentRange": { + "end": 27, + "start": 27, + }, + "context": { + "code": "", + "name": "other-item", + "type": "function", + }, + "description": "Other item + ", + "group": [ + "undefined", + ], + "name": "other-item", + }, + { + "access": "public", + "commentRange": { + "end": 31, + "start": 31, + }, + "context": { + "code": "", + "name": "yet-another-item", + "type": "function", + }, + "description": "Yet another item + ", + "group": [ + "undefined", + ], + "name": "yet-another-item", + }, + ] + `); +}); diff --git a/src/sassdoc-parser-two.fixture.scss b/src/sassdoc-parser-two.fixture.scss deleted file mode 100644 index 24e27bb..0000000 --- a/src/sassdoc-parser-two.fixture.scss +++ /dev/null @@ -1,2 +0,0 @@ -/// @todo Document me -$stardew: #000000; diff --git a/src/sassdoc-parser.fixture.scss b/src/sassdoc-parser.fixture.scss deleted file mode 100644 index 83774ea..0000000 --- a/src/sassdoc-parser.fixture.scss +++ /dev/null @@ -1,2 +0,0 @@ -/// @todo Document me -$valley: #000000; diff --git a/src/sassdoc-parser.ts b/src/sassdoc-parser.ts index 1d522b6..90917b3 100644 --- a/src/sassdoc-parser.ts +++ b/src/sassdoc-parser.ts @@ -1,32 +1,35 @@ -import ScssCommentParser, { +import AnnotationsApi, { type BuiltInAnnotationNames } from "./annotation.js"; +import SassCommentParser, { type Annotations, type ParserConfig, -} from "scss-comment-parser"; -import AnnotationsApi, { type BuiltInAnnotationNames } from "./annotation.js"; +} from "./sass-comment-parser.js"; import sorter from "./sorter.js"; import type { ParseResult } from "./types.js"; import { removeReduntantWhitespace } from "./utils.js"; class Parser { annotations: AnnotationsApi; - scssParser: ScssCommentParser; + commentParser: SassCommentParser; constructor(parserConfig?: ParserConfig) { this.annotations = new AnnotationsApi(); - this.scssParser = new ScssCommentParser( + this.commentParser = new SassCommentParser( this.annotations.list as unknown as Annotations, parserConfig, ); - this.scssParser.commentParser.on("warning", (warning: Error) => { + this.commentParser.commentParser.on("warning", (warning: Error) => { console.warn(warning.message); }); } - async parseString(code: string, id?: string): Promise { - let data = this.scssParser.parse( + async parseString( + code: string, + options?: ParseOptions, + ): Promise { + let data = this.commentParser.parse( removeReduntantWhitespace(code), - id, - ) as Array; + options?.id, + ); data = sorter(data); data = data.map((d) => { @@ -50,11 +53,11 @@ class Parser { return Promise.all(promises).then(() => data); } - parseStringSync(code: string, id?: string): ParseResult[] { - let data = this.scssParser.parse( + parseStringSync(code: string, options?: ParseOptions): ParseResult[] { + let data = this.commentParser.parse( removeReduntantWhitespace(code), - id, - ) as Array; + options?.id, + ); data = sorter(data); data = data.map((d) => { @@ -86,35 +89,45 @@ export type ParseOptions = { }; /** - * Try to parse any SassDoc in the SCSS input + * Try to parse any SassDoc in the input * - * @example + * @example SCSS * await parse(` * /// Main color * $stardew: #ffffff; * `); + * @example Indented + * await parse(` + * /// Main color + * $stardew: #ffffff + * `); */ export async function parse( code: string, options?: ParseOptions, ): Promise> { const parser = new Parser(options?.parserConfig); - return await parser.parseString(code, options?.id); + return await parser.parseString(code, options); } /** - * Try to parse any SassDoc in the SCSS input + * Try to parse any SassDoc in the input * * @example - * parse(` + * parseSync(` * /// Main color * $stardew: #ffffff; * `); + * @example Indented + * parseSync(` + * /// Main color + * $stardew: #ffffff + * `); */ export function parseSync( code: string, options?: ParseOptions, ): Array { const parser = new Parser(options?.parserConfig); - return parser.parseStringSync(code, options?.id); + return parser.parseStringSync(code, options); } diff --git a/src/sorter.ts b/src/sorter.ts index 5c8113c..e143a0a 100644 --- a/src/sorter.ts +++ b/src/sorter.ts @@ -8,7 +8,7 @@ export default function sort(data: Array) { b.group?.[0].toLowerCase() || "", ) || compare(a.file?.path || "", b.file?.path || "") || - compare(a.context.line.start, b.context.line.start) + compare(a.context.line?.start, b.context.line?.start) ); }); } diff --git a/types/scss-comment-parser.d.ts b/types/cdocparser.d.ts similarity index 56% rename from types/scss-comment-parser.d.ts rename to types/cdocparser.d.ts index 0ae71fc..1406bf4 100644 --- a/types/scss-comment-parser.d.ts +++ b/types/cdocparser.d.ts @@ -1,10 +1,8 @@ /// -declare module "scss-comment-parser" { +declare module "cdocparser" { import { EventEmitter } from "events"; - export default Parser; - export interface Annotation { name: string; alias?: string[]; @@ -17,6 +15,7 @@ declare module "scss-comment-parser" { } export interface ParserConfig { + syntax?: "scss" | "indented"; lineComment?: boolean; blockComment?: boolean; lineCommentStyle?: string; @@ -51,35 +50,26 @@ declare module "scss-comment-parser" { context: Context; } - class CommentParser extends EventEmitter { - parse(code: string, id?: string): Array; - } - - class Parser { - /** - * @see cdocparser - */ + export class CommentParser extends EventEmitter { constructor(annotations: Annotations, config?: ParserConfig); + parse(comments: ExtractedComment[], id?: string): ParseResult[]; + } - commentParser: CommentParser; + export type ContextParser = ( + code: string, + lineNumberFor: (index?: number) => number, + ) => Context; - parse(code: string, id?: string): Array; + type ExtractedComment = { + lines: string[]; + type: "block" | "line" | "poster"; + commentRange: Range; + context: Context; + }; - /** - * SCSS Context Parser - */ - contextParser( - contextCode: string, - lineNumberFor?: (index: number) => number, - ): Context; + export class CommentExtractor { + constructor(contextParser: ContextParser, config?: ParserConfig); - /** - * Extract the code following `offset` in `code` buffer, - * delimited by braces. - * - * `offset` should be set to the position of the opening brace. If not, - * the function will jump to the next opening brace. - */ - extractCode(code: string, offset: number): string; + extract(code: string): ExtractedComment[]; } }