diff --git a/.gitignore b/.gitignore index 412759161ed..9a5b85c4556 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules/ /playwright-report/ /playwright/.cache/ $__StoryList.tid +types/core/ diff --git a/core/modules/parsers/base.js b/core/modules/parsers/base.js new file mode 100644 index 00000000000..49bb5e36040 --- /dev/null +++ b/core/modules/parsers/base.js @@ -0,0 +1,62 @@ +/*\ +title: $:/core/modules/parsers/base.js +type: application/javascript +module-type: library +\*/ + +/** + * Represents an attribute in a parse tree node + * + * @typedef {Object} ParseTreeAttribute + * @property {number} [end] - End position of attribute in source text + * @property {string} [name] - Name of attribute + * @property {number} [start] - Start position of attribute in source text + * @property {'string' | 'number' | 'bigint' | 'boolean' | 'macro' | 'macro-parameter'} type - Type of attribute + * @property {string | IMacroCallParseTreeNode} value - Actual value of attribute + */ + +/** + * Base structure for a parse node + * + * @typedef {Object} ParseTreeNode + * @property {string} type - Type of widget that will render this node + * @property {string} rule - Parse rule that generated this node. One rule can generate multiple types of nodes + * @property {number} start - Rule start marker in source text + * @property {number} end - Rule end marker in source text + * @property {Record} [attributes] - Attributes of widget + * @property {ParseTreeNode[]} [children] - Array of child parse nodes + */ + +/** + * Base class for parsers. This only provides typing + * + * @class + * @param {string} type - Content type of text to be parsed + * @param {string} text - Text to be parsed + * @param {Object} options - Parser options + * @param {boolean} [options.parseAsInline=false] - If true, text will be parsed as an inline run + * @param {Object} options.wiki - Reference to wiki store in use + * @param {string} [options._canonical_uri] - Optional URI of content if text is missing or empty + * @param {boolean} [options.configTrimWhiteSpace=false] - If true, parser trims white space + */ +function Parser(type, text, options) { + /** + * Result AST + * @type {ParseTreeNode[]} + */ + this.tree = []; + + /** + * Original text without modifications + * @type {string} + */ + this.source = text; + + /** + * Source content type in MIME format + * @type {string} + */ + this.type = type; +} + +exports.Parser = Parser; diff --git a/core/modules/parsers/wikiparser/rules/codeblock.js b/core/modules/parsers/wikiparser/rules/codeblock.js index 6c3480566b9..f6e9463f3e5 100644 --- a/core/modules/parsers/wikiparser/rules/codeblock.js +++ b/core/modules/parsers/wikiparser/rules/codeblock.js @@ -12,6 +12,27 @@ Wiki text rule for code blocks. For example: ``` \*/ + +/** + * @typedef {import("$:/core/modules/parsers/base.js").ParseTreeAttribute} ParseTreeAttribute + * @typedef {import('../wikirulebase.js').WikiRuleBase} WikiRuleBase + * @typedef {import('../../base.js').Parser} Parser + * @typedef {typeof exports & WikiRuleBase} ThisRule + */ + +/** + * Represents the `codeblock` rule. + * + * @typedef {Object} ParseTreeCodeblockNode + * @property {"codeblock"} rule + * @property {"codeblock"} type + * @property {number} start + * @property {number} end + * @property {Object} attributes + * @property {ParseTreeAttribute} attributes.code + * @property {ParseTreeAttribute} attributes.language + */ + (function(){ /*jslint node: true, browser: true */ @@ -27,6 +48,12 @@ exports.init = function(parser) { this.matchRegExp = /```([\w-]*)\r?\n/mg; }; +/** + * Parses the code block and returns an array of `codeblock` widgets. + * + * @this {ThisRule} + * @returns {ParseTreeCodeblockNode[]} An array containing a single codeblock widget object. + */ exports.parse = function() { var reEnd = /(\r?\n```$)/mg; var languageStart = this.parser.pos + 3, diff --git a/core/modules/parsers/wikiparser/rules/html.js b/core/modules/parsers/wikiparser/rules/html.js index 61c4ad9e1ec..b553515136b 100644 --- a/core/modules/parsers/wikiparser/rules/html.js +++ b/core/modules/parsers/wikiparser/rules/html.js @@ -17,6 +17,34 @@ This is a widget invocation }}} \*/ + + +/** + * @typedef {import("$:/core/modules/parsers/base.js").ParseTreeAttribute} ParseTreeAttribute + * @typedef {import('../wikirulebase.js').WikiRuleBase} WikiRuleBase + * @typedef {import('../../base.js').Parser} Parser + * @typedef {typeof exports & WikiRuleBase & {nextTag?:ParseTreeHtmlNode;}} ThisRule + */ + +/** + * Represents the parser `html` rule + * + * @typedef {Object} ParseTreeHtmlNode + * @property {"html"} rule + * @property {"element"} type + * @property {keyof HTMLElementTagNameMap} tag + * @property {number} start + * @property {number} end + * @property {Record} attributes - Contains attributes of HTML element + * @property {boolean} isSelfClosing - If tag is self-closing + * @property {boolean} isBlock - If tag is a block element + * @property {number} openTagStart + * @property {number} openTagEnd + * @property {number} closeTagStart + * @property {number} closeTagEnd + * @property {ParseTreeHtmlNode[]} [children] + */ + (function(){ /*jslint node: true, browser: true */ @@ -26,10 +54,18 @@ This is a widget invocation exports.name = "html"; exports.types = {inline: true, block: true}; +/** + * @param {Parser} parser + */ exports.init = function(parser) { this.parser = parser; }; +/** + * @this {ThisRule} + * @param {number} startPos + * @returns {number | undefined} Start position of next HTML tag + */ exports.findNextMatch = function(startPos) { // Find the next tag this.nextTag = this.findNextTag(this.parser.source,startPos,{ @@ -38,11 +74,16 @@ exports.findNextMatch = function(startPos) { return this.nextTag ? this.nextTag.start : undefined; }; -/* -Parse the most recent match -*/ +/** + * Parse most recent match + * @this {ThisRule} + * @returns {ParseTreeHtmlNode[]} Array containing parsed HTML tag object + */ exports.parse = function() { - // Retrieve the most recent match so that recursive calls don't overwrite it + /** + * @type {ParseTreeHtmlNode} + * Retrieve the most recent match so that recursive calls don't overwrite it + */ var tag = this.nextTag; if (!tag.isSelfClosing) { tag.openTagStart = tag.start; @@ -161,6 +202,15 @@ exports.parseTag = function(source,pos,options) { return node; }; +/** + * Find the next HTML tag in the source + * + * @this {ThisRule} + * @param {string} source + * @param {number} pos - Position to start searching from + * @param {Object} options + * @returns {Object|null} Parsed tag object or null if no valid tag is found + */ exports.findNextTag = function(source,pos,options) { // A regexp for finding candidate HTML tags var reLookahead = /<([a-zA-Z\-\$\.]+)/g; diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js index 854171d197f..653481670ef 100644 --- a/core/modules/parsers/wikiparser/wikiparser.js +++ b/core/modules/parsers/wikiparser/wikiparser.js @@ -25,16 +25,18 @@ Attributes are stored as hashmaps of the following objects: /*global $tw: false */ "use strict"; -/* -type: content type of text -text: text to be parsed -options: see below: - parseAsInline: true to parse text as inline instead of block - wiki: reference to wiki to use - _canonical_uri: optional URI of content if text is missing or empty - configTrimWhiteSpace: true to trim whitespace -*/ -var WikiParser = function(type,text,options) { +/** + * @typedef {import('../base').Parser} Parser + */ + +/** + * WikiParser class for parsing text of a specified MIME type. + * + * @class + * @extends {Parser} + * @constructor + */ +function WikiParser(type,text,options) { this.wiki = options.wiki; var self = this; // Check for an externally linked tiddler @@ -99,8 +101,11 @@ var WikiParser = function(type,text,options) { // Return the parse tree }; -/* -*/ +/** + * Load a remote tiddler from a given URL. + * + * @param {string} url - The URL of the remote tiddler to load. + */ WikiParser.prototype.loadRemoteTiddler = function(url) { var self = this; $tw.utils.httpRequest({ diff --git a/core/modules/parsers/wikiparser/wikirulebase.js b/core/modules/parsers/wikiparser/wikirulebase.js index 8aa960ef790..f2a319942e1 100644 --- a/core/modules/parsers/wikiparser/wikirulebase.js +++ b/core/modules/parsers/wikiparser/wikirulebase.js @@ -12,22 +12,46 @@ Base class for wiki parser rules /*global $tw: false */ "use strict"; -/* -This constructor is always overridden with a blank constructor, and so shouldn't be used -*/ -var WikiRuleBase = function() { +/** + * @typedef {import('../base').Parser} Parser + */ + +/** + * Base class for wiki rules. + * This constructor is always overridden with a blank constructor, and so shouldn't be used + * + * @class + * @constructor + */ +function WikiRuleBase() { + /** + * Inject by parser + * @type {Record<"pragma"|"block"|"inline", boolean>} + */ + this.is = {}; + /** + * @type {RegExp} + */ + this.matchRegExp; }; -/* -To be overridden by individual rules -*/ +/** + * Initialize rule with given parser instance + * To be overridden by individual rules + * + * @param {Parser} parser - Parser instance to initialize with + */ WikiRuleBase.prototype.init = function(parser) { this.parser = parser; }; -/* -Default implementation of findNextMatch uses RegExp matching -*/ +/** + * Default implementation of findNextMatch uses RegExp matching + * Find next match in source starting from given position using RegExp matching + * + * @param {number} startPos - Position to start searching from + * @returns {number|undefined} Index of next match or undefined if no match is found + */ WikiRuleBase.prototype.findNextMatch = function(startPos) { this.matchRegExp.lastIndex = startPos; this.match = this.matchRegExp.exec(this.parser.source); diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js index eb84fab4a8e..5759312c76d 100755 --- a/core/modules/widgets/widget.js +++ b/core/modules/widgets/widget.js @@ -12,22 +12,30 @@ Widget base class /*global $tw: false */ "use strict"; -/* -Create a widget object for a parse tree node - parseTreeNode: reference to the parse tree node to be rendered - options: see below -Options include: - wiki: mandatory reference to wiki associated with this render tree - parentWidget: optional reference to a parent renderer node for the context chain - document: optional document object to use instead of global document -*/ -var Widget = function(parseTreeNode,options) { +/** + * Widget class for creating a widget object for a parse tree node. + * + * @class + * @constructor + * @param {Object} parseTreeNode - Reference to the parse tree node to be rendered. + * @param {Object} options - Options for the widget. + * @param {Object} options.wiki - Mandatory reference to the wiki associated with this render tree. + * @param {Widget} [options.parentWidget] - Optional reference to a parent renderer node for the context chain. + * @param {Document} [options.document] - Optional document object to use instead of the global document. + */ +function Widget(parseTreeNode,options) { this.initialise(parseTreeNode,options); }; -/* -Initialise widget properties. These steps are pulled out of the constructor so that we can reuse them in subclasses -*/ +/** + * Initialise widget properties. These steps are pulled out of the constructor so that we can reuse them in subclasses. + * + * @param {Object} parseTreeNode - Reference to the parse tree node to be rendered. + * @param {Object} options - Options for the widget. + * @param {Object} options.wiki - Mandatory reference to the wiki associated with this render tree. + * @param {Widget} [options.parentWidget] - Optional reference to a parent renderer node for the context chain. + * @param {Document} [options.document] - Optional document object to use instead of the global document. + */ Widget.prototype.initialise = function(parseTreeNode,options) { // Bail if parseTreeNode is undefined, meaning that the widget constructor was called without any arguments so that it can be subclassed if(parseTreeNode === undefined) { @@ -64,9 +72,12 @@ Widget.prototype.initialise = function(parseTreeNode,options) { } }; -/* -Render this widget into the DOM -*/ +/** + * Render this widget into the DOM. + * + * @param {Element} parent - The parent DOM node to render into. + * @param {Element} nextSibling - The next sibling DOM node to render before. + */ Widget.prototype.render = function(parent,nextSibling) { this.parentDomNode = parent; this.execute(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 5673c9e3baa..2ca84906d54 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -16,13 +16,8 @@ Adds the following properties to the wiki object: * `globalCache` is a hashmap by cache name of cache objects that are cleared whenever any tiddler change occurs \*/ -(function(){ -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; - -var widget = require("$:/core/modules/widgets/widget.js"); +var Widget = require("$:/core/modules/widgets/widget.js").widget; var USER_NAME_TITLE = "$:/status/UserName", TIMESTAMP_DISABLE_TITLE = "$:/config/TimestampDisable"; @@ -1041,19 +1036,29 @@ exports.initParsers = function(moduleType) { } }; -/* -Parse a block of text of a specified MIME type - type: content type of text to be parsed - text: text - options: see below -Options include: - parseAsInline: if true, the text of the tiddler will be parsed as an inline run - _canonical_uri: optional string of the canonical URI of this content -*/ +/** + * @typedef {import('$:/core/modules/parsers/base.js').Parser} Parser + */ + +/** + * Parse a block of text of a specified MIME type + * + * @param {string} type - Content type of text to be parsed + * @param {string} text - Text to be parsed + * @param {Object} [options] - Options for parsing + * @param {boolean} [options.parseAsInline=false] - If true, text will be parsed as an inline run + * @param {string} [options._canonical_uri] - Optional string of canonical URI of this content + * @param {string} [options.defaultType="text/vnd.tiddlywiki"] - Default type to use if no parser is found for specified type + * @param {boolean} [options.configTrimWhiteSpace=false] - If true, trims white space according to configuration + * + * @returns {Parser | null} Parser instance or null if no parser is found + */ exports.parseText = function(type,text,options) { text = text || ""; options = options || {}; - // Select a parser + /** + * Select a parser + */ var Parser = $tw.Wiki.parsers[type]; if(!Parser && $tw.utils.getFileExtensionInfo(type)) { Parser = $tw.Wiki.parsers[$tw.utils.getFileExtensionInfo(type).type]; @@ -1143,14 +1148,16 @@ exports.getTextReferenceParserInfo = function(title,field,index,options) { return parserInfo; } -/* -Parse a block of text of a specified MIME type - text: text on which to perform substitutions - widget - options: see below -Options include: - substitutions: an optional array of substitutions -*/ +/** + * Parse a block of text of a specified MIME type and perform substitutions. + * + * @param {string} text - The text on which to perform substitutions. + * @param {Widget} widget - The widget context used for variable substitution. + * @param {Object} [options] - Options for substitutions. + * @param {Array<{name: string, value: string}>} [options.substitutions] - An optional array of substitutions. + * + * @returns {string} The text with substitutions applied. + */ exports.getSubstitutedText = function(text,widget,options) { options = options || {}; text = text || ""; @@ -1171,15 +1178,17 @@ exports.getSubstitutedText = function(text,widget,options) { }); }; -/* -Make a widget tree for a parse tree -parser: parser object -options: see below -Options include: -document: optional document to use -variables: hashmap of variables to set -parentWidget: optional parent widget for the root node -*/ +/** + * Create a widget tree for a parse tree. + * + * @param {Object} parser - The parser object containing the parse tree. + * @param {Object} [options] - Options for creating the widget tree. + * @param {Document} [options.document] - Optional document to use. + * @param {Object} [options.variables] - Hashmap of variables to set. + * @param {Widget} [options.parentWidget] - Optional parent widget for the root node. + * + * @returns {Widget} The root widget of the created widget tree. + */ exports.makeWidget = function(parser,options) { options = options || {}; var widgetNode = { @@ -1204,7 +1213,7 @@ exports.makeWidget = function(parser,options) { // Add in the supplied parse tree nodes currWidgetNode.children = parser ? parser.tree : []; // Create the widget - return new widget.widget(widgetNode,{ + return new Widget(widgetNode,{ wiki: this, document: options.document || $tw.fakeDocument, parentWidget: options.parentWidget @@ -1487,9 +1496,13 @@ exports.search = function(text,options) { return results; }; -/* -Trigger a load for a tiddler if it is skinny. Returns the text, or undefined if the tiddler is missing, null if the tiddler is being lazily loaded. -*/ +/** + * Trigger a load for a tiddler if it is skinny. Returns the text, or undefined if the tiddler is missing, null if the tiddler is being lazily loaded. + * + * @param {string} title - The title of the tiddler. + * @param {string} [defaultText] - The default text to return if the tiddler is missing. + * @returns {string | null | undefined} - The text of the tiddler, undefined if the tiddler is missing, or null if the tiddler is being lazily loaded. + */ exports.getTiddlerText = function(title,defaultText) { var tiddler = this.getTiddler(title); // Return undefined if the tiddler isn't found @@ -1781,5 +1794,3 @@ exports.slugify = function(title,options) { } return slug; }; - -})(); diff --git a/package.json b/package.json index 4dbd390878c..d036a46c66c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bin": { "tiddlywiki": "./tiddlywiki.js" }, + "types": "./types/tw.d.ts", "main": "./boot/boot.js", "repository": { "type": "git", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..5398d26ba2b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "paths": { + // Allow `import('$:/core/modules/...')` instead of `import('../../core/modules/...')`. Only works inside this project. + "$:/core/*": ["core/*"] + }, + "baseUrl": ".", + "rootDir": ".", + "noImplicitAny": false, + "strict": false, + "allowJs": true, + "checkJs": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationDir": "types/", + "declarationMap": true, + "emitDeclarationOnly": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "Node16", + "outDir": "types/", + "skipLibCheck": true, + "target": "ESNext" + }, + "include": ["./core/**/*.js", "./core/**/*.d.ts", "types/*.d.ts"], + // Exclude the generated types from the core folder + "exclude": ["types/core",] +} diff --git a/types/ast.d.ts b/types/ast.d.ts new file mode 100644 index 00000000000..ca8fdc90b5e --- /dev/null +++ b/types/ast.d.ts @@ -0,0 +1,6 @@ +import { ParseTreeCodeblockNode } from '$:/core/modules/parsers/wikiparser/rules/codeblock.js'; +export { ParseTreeCodeblockNode } from '$:/core/modules/parsers/wikiparser/rules/codeblock.js'; +import { ParseTreeHtmlNode } from '$:/core/modules/parsers/wikiparser/rules/html.js'; +export { ParseTreeHtmlNode } from '$:/core/modules/parsers/wikiparser/rules/html.js'; + +export type WikiASTNode = ParseTreeCodeblockNode | ParseTreeHtmlNode; diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 00000000000..04a2433755d --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "paths": { + // Allow `import('$:/core/modules/...')` instead of `import('../../core/modules/...')`. Only works inside this project. + "$:/core/*": ["core/*"] + }, + "baseUrl": "./", + "rootDir": "./", + "noEmit": true, + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "declaration": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "Node16", + "skipLibCheck": true, + "target": "ESNext" + }, + "include": ["./core/**/*.d.ts"] +} diff --git a/types/tw.d.ts b/types/tw.d.ts new file mode 100644 index 00000000000..17d7c0b274f --- /dev/null +++ b/types/tw.d.ts @@ -0,0 +1,7 @@ +import * as Wiki from '$:/core/modules/wiki'; + +declare global { + var $tw: { + wiki: typeof Wiki; + }; +}