diff --git a/__tests__/api-writer/glua-api-writer.spec.ts b/__tests__/api-writer/glua-api-writer.spec.ts index 0b229ce..704edd7 100644 --- a/__tests__/api-writer/glua-api-writer.spec.ts +++ b/__tests__/api-writer/glua-api-writer.spec.ts @@ -6,7 +6,7 @@ import { apiDefinition as structApiDefinition, markup as structMarkup, json as s import { markup as panelMarkup, apiDefinition as panelApiDefinition } from '../test-data/offline-sites/gmod-wiki/panel-slider'; import { markup as multiReturnFuncMarkup, apiDefinition as multiReturnFuncApiDefinition } from '../test-data/offline-sites/gmod-wiki/library-function-concommand-gettable'; import { markup as varargsFuncMarkup, apiDefinition as varargsFuncApiDefinition } from '../test-data/offline-sites/gmod-wiki/library-function-coroutine-resume'; -import { Enum, LibraryFunction, WikiPage, WikiPageMarkupScraper } from '../../src/scrapers/wiki-page-markup-scraper'; +import { Enum, LibraryFunction, PanelFunction, WikiPage, WikiPageMarkupScraper } from '../../src/scrapers/wiki-page-markup-scraper'; import { GluaApiWriter } from '../../src/api-writer/glua-api-writer'; import fetchMock from "jest-fetch-mock"; @@ -107,12 +107,12 @@ describe('GLua API Writer', () => { url: 'na', arguments: [ { - args: [ { + args: [{ name: 'intensity', type: 'number', description: 'The intensity of the explosion.', default: '1000', - } ] + }] } ], returns: [ @@ -397,6 +397,28 @@ describe('GLua API Writer', () => { expect(api).toEqual(`---![(Client)](https://github.com/user-attachments/assets/a5f6ba64-374d-42f0-b2f4-50e5c964e808) Returns where on the screen the specified position vector would appear.\n---\n---[View wiki](na)\n---@return ToScreenData # The created Structures/ToScreenData.\nfunction Vector.ToScreen() end\n\n`); }); + it('should support Panel type', () => { + const writer = new GluaApiWriter(); + const api = writer.writePage({ + name: 'GetVBar', + address: 'DScrollPanel.GetVBar', + parent: 'DScrollPanel', + isPanelFunction: 'yes', + description: 'Returns the vertical scroll bar of the panel.', + realm: 'client', + type: 'panelfunc', + url: 'na', + returns: [ + { + type: 'Panel{DVScrollBar}', + description: 'The DVScrollBar.', + }, + ], + }); + + expect(api).toEqual(`---![(Client)](https://github.com/user-attachments/assets/a5f6ba64-374d-42f0-b2f4-50e5c964e808) Returns the vertical scroll bar of the panel.\n---\n---[View wiki](na)\n---@return DVScrollBar # The DVScrollBar.\nfunction DScrollPanel:GetVBar() end\n\n`); + }); + // number{ENUM_NAME} -> ENUM_NAME it('should support enum type', () => { const writer = new GluaApiWriter(); diff --git a/__tests__/api-writer/markdownify.spec.ts b/__tests__/api-writer/markdownify.spec.ts new file mode 100644 index 0000000..4d44955 --- /dev/null +++ b/__tests__/api-writer/markdownify.spec.ts @@ -0,0 +1,14 @@ +import { markdownifyDescription } from '../../src/scrapers/wiki-page-markup-scraper'; +import * as cheerio from 'cheerio'; + +describe('markdownifyDescription', () => { + it('should handle spaces in links', async () => { + const textWithUrl = 'This is a link with spaces.'; + const $ = cheerio.load(textWithUrl, { xmlMode: true }); + const result = markdownifyDescription($, $('test')); + + expect(result).toBe( + 'This is a [link with spaces](https://wiki.facepunch.com/gmod/link_with_spaces).' + ); + }); +}); diff --git a/__tests__/api-writer/write-type.spec.ts b/__tests__/api-writer/write-type.spec.ts new file mode 100644 index 0000000..ee855bf --- /dev/null +++ b/__tests__/api-writer/write-type.spec.ts @@ -0,0 +1,35 @@ +import { GluaApiWriter } from '../../src/api-writer/glua-api-writer'; + +describe('writeType', () => { + describe('callback functions', () => { + it('should write parameter names', async () => { + const result = GluaApiWriter.transformType('function', { + arguments: [ + { type: 'number', name: 'count' }, + { type: 'string', name: 'total' }, + ], + returns: [ + { type: 'number', name: 'sum' }, + { type: 'string', name: 'limit' }, + ], + }); + + expect(result).toEqual('fun(count: number, total: string):(sum: number, limit: string)'); + }); + + it('should write ret1, ret2, etc when parameter names are missing', async () => { + const result = GluaApiWriter.transformType('function', { + arguments: [ + { type: 'number', name: 'count' }, + { type: 'string', name: '' }, + ], + returns: [ + { type: 'number', name: '' }, + { type: 'string', name: '' }, + ], + }); + + expect(result).toEqual('fun(count: number, arg1: string):(ret0: number, ret1: string)'); + }); + }); +}); diff --git a/__tests__/test-data/offline-sites/gmod-wiki/panel-slider.ts b/__tests__/test-data/offline-sites/gmod-wiki/panel-slider.ts index 5546ef0..71421d2 100644 --- a/__tests__/test-data/offline-sites/gmod-wiki/panel-slider.ts +++ b/__tests__/test-data/offline-sites/gmod-wiki/panel-slider.ts @@ -37,11 +37,7 @@ end `; export const apiDefinition = -`--- ---- ---- ---- A simple slider featuring an numeric display. ---- + `--- A simple slider featuring an numeric display. ---@deprecated Panel:SetActionFunction and Panel:PostMessage. Use DNumSlider instead. ---@class Slider : Panel local Slider = {}\n\n`; diff --git a/__tests__/test-data/offline-sites/gmod-wiki/struct-custom-entity-fields.ts b/__tests__/test-data/offline-sites/gmod-wiki/struct-custom-entity-fields.ts index f968c13..d294176 100644 --- a/__tests__/test-data/offline-sites/gmod-wiki/struct-custom-entity-fields.ts +++ b/__tests__/test-data/offline-sites/gmod-wiki/struct-custom-entity-fields.ts @@ -47,12 +47,10 @@ This can also be set from map, see Sandbox Specific Mapping `; export const apiDefinition = -`--- ---- Information about custom fields **all** entities can have. + `--- Information about custom fields **all** entities can have. --- --- See also [Structures/ENT](https://wiki.facepunch.com/gmod/Structures/ENT) --- ---- ---[View wiki](https://wiki.facepunch.com/gmod/Custom_Entity_Fields) ---@class Custom_Entity_Fields local Custom_Entity_Fields = {} @@ -136,94 +134,94 @@ Custom_Entity_Fields.m_RenderOrigin = nil Custom_Entity_Fields.m_RenderAngles = nil\n\n`; export const json = { - name: 'Custom_Entity_Fields', - address: 'Custom_Entity_Fields', - type: 'struct', - fields: [ - { - name: 'GetEntityDriveMode', - type: 'function', - description: '`Serverside`, Sandbox and Sandbox derived only.\n\nCalled by the Drive property to override the default drive type, which is `drive_sandbox`.', - }, - { - name: 'OnEntityCopyTableFinish', - type: 'function', - description: 'Documented at ENTITY:OnEntityCopyTableFinish.', - }, - { - name: 'PostEntityCopy', - type: 'function', - description: 'Documented at ENTITY:PostEntityCopy.', - }, - { - name: 'PostEntityPaste', - type: 'function', - description: 'Documented at ENTITY:PostEntityPaste.', - }, - { - name: 'PreEntityCopy', - type: 'function', - description: 'Documented at ENTITY:PreEntityCopy.', - }, - { - name: 'OnDuplicated', - type: 'function', - description: 'Documented at ENTITY:OnDuplicated.', - }, - { - name: 'PhysgunDisabled', - type: 'boolean', - description: '`Shared`, Sandbox or Sandbox derived only.\n\nIf set to `true`, physgun will not be able to pick this entity up. This can also be set from map, see Sandbox Specific Mapping', - }, - { - name: 'PhysgunPickup', - type: 'function', - description: '`Shared`, Sandbox or Sandbox derived only.\n\nCalled from GM:PhysgunPickup, overrides `PhysgunDisabled`', - }, - { - name: 'm_tblToolsAllowed', - type: 'table', - description: '`Shared`, Sandbox or Sandbox derived only.\n\nControls which tools **and** properties can be used on this entity. Format is a list of strings where each string is the tool or property classname.\n\nThis can also be set from map, see Sandbox Specific Mapping', - }, - { - name: 'GravGunPickupAllowed', - type: 'function', - description: 'Documented at ENTITY:GravGunPickupAllowed.', - }, - { - name: 'GravGunPunt', - type: 'function', - description: 'Documented at ENTITY:GravGunPunt.', - }, - { - name: 'CanProperty', - type: 'function', - description: 'Documented at ENTITY:CanProperty.', - }, - { - name: 'CanTool', - type: 'function', - description: 'Documented at ENTITY:CanTool.', - }, - { - name: 'CalcAbsolutePosition', - type: 'function', - description: 'Documented at ENTITY:CalcAbsolutePosition.', - }, - { - name: 'RenderOverride', - type: 'function', - description: 'Documented at ENTITY:RenderOverride.', - }, - { - name: 'm_RenderOrigin', - type: 'Vector', - description: '(Clientside) Do not use.', - }, - { - name: 'm_RenderAngles', - type: 'Angle', - description: '(Clientside) Do not use.', - }, - ], + name: 'Custom_Entity_Fields', + address: 'Custom_Entity_Fields', + type: 'struct', + fields: [ + { + name: 'GetEntityDriveMode', + type: 'function', + description: '`Serverside`, Sandbox and Sandbox derived only.\n\nCalled by the Drive property to override the default drive type, which is `drive_sandbox`.', + }, + { + name: 'OnEntityCopyTableFinish', + type: 'function', + description: 'Documented at ENTITY:OnEntityCopyTableFinish.', + }, + { + name: 'PostEntityCopy', + type: 'function', + description: 'Documented at ENTITY:PostEntityCopy.', + }, + { + name: 'PostEntityPaste', + type: 'function', + description: 'Documented at ENTITY:PostEntityPaste.', + }, + { + name: 'PreEntityCopy', + type: 'function', + description: 'Documented at ENTITY:PreEntityCopy.', + }, + { + name: 'OnDuplicated', + type: 'function', + description: 'Documented at ENTITY:OnDuplicated.', + }, + { + name: 'PhysgunDisabled', + type: 'boolean', + description: '`Shared`, Sandbox or Sandbox derived only.\n\nIf set to `true`, physgun will not be able to pick this entity up. This can also be set from map, see Sandbox Specific Mapping', + }, + { + name: 'PhysgunPickup', + type: 'function', + description: '`Shared`, Sandbox or Sandbox derived only.\n\nCalled from GM:PhysgunPickup, overrides `PhysgunDisabled`', + }, + { + name: 'm_tblToolsAllowed', + type: 'table', + description: '`Shared`, Sandbox or Sandbox derived only.\n\nControls which tools **and** properties can be used on this entity. Format is a list of strings where each string is the tool or property classname.\n\nThis can also be set from map, see Sandbox Specific Mapping', + }, + { + name: 'GravGunPickupAllowed', + type: 'function', + description: 'Documented at ENTITY:GravGunPickupAllowed.', + }, + { + name: 'GravGunPunt', + type: 'function', + description: 'Documented at ENTITY:GravGunPunt.', + }, + { + name: 'CanProperty', + type: 'function', + description: 'Documented at ENTITY:CanProperty.', + }, + { + name: 'CanTool', + type: 'function', + description: 'Documented at ENTITY:CanTool.', + }, + { + name: 'CalcAbsolutePosition', + type: 'function', + description: 'Documented at ENTITY:CalcAbsolutePosition.', + }, + { + name: 'RenderOverride', + type: 'function', + description: 'Documented at ENTITY:RenderOverride.', + }, + { + name: 'm_RenderOrigin', + type: 'Vector', + description: '(Clientside) Do not use.', + }, + { + name: 'm_RenderAngles', + type: 'Angle', + description: '(Clientside) Do not use.', + }, + ], }; diff --git a/__tests__/utils/string.spec.ts b/__tests__/utils/string.spec.ts index f96bb5f..786eb66 100644 --- a/__tests__/utils/string.spec.ts +++ b/__tests__/utils/string.spec.ts @@ -1,4 +1,4 @@ -import { removeNewlines, toLowerCamelCase, putCommentBeforeEachLine, safeFileName } from '../../src/utils/string'; +import { removeNewlines, toLowerCamelCase, wrapInComment, safeFileName, unindentText } from '../../src/utils/string'; describe('toLowerCamelCase', () => { it('should convert a string to lowerCamelCase', () => { @@ -35,21 +35,74 @@ describe('putCommentBeforeEachLine', () => { ['hello\n\n\n\nworld', '--- hello\n---\n--- world'], ['hello\r\n\r\n\r\n\r\nworld', '--- hello\n---\n--- world'], ])('should put a comment before each line', (input, expected, skipFirstLine = false) => { - expect(putCommentBeforeEachLine(input, skipFirstLine)).toBe(expected); + expect(wrapInComment(input, skipFirstLine)).toBe(expected); + }); +}); + +describe('wrapInComment', () => { + it.each([ + [ + `The following specifiers are exclusive to LuaJIT: + +| Format | Description | Example of the output | +|:------:|:-----------:|:---------------------:| +| %p | Returns pointer to supplied structure (table/function) | \`0xf20a8968\` | +| %q | Formats a string between double quotes, using escape sequences when necessary to ensure that it can safely be read back by the Lua interpreter | \`"test\\1\\2test"\` |`, + `The following specifiers are exclusive to LuaJIT: +--[[ + +| Format | Description | Example of the output | +|:------:|:-----------:|:---------------------:| +| %p | Returns pointer to supplied structure (table/function) | \`0xf20a8968\` | +| %q | Formats a string between double quotes, using escape sequences when necessary to ensure that it can safely be read back by the Lua interpreter | \`"test\\1\\2test"\` | +--]]`, + true, + ], + [ + `The following specifiers are exclusive to LuaJIT: + +| Format | Description | Example of the output | +| ------ | ----------- | --------------------- | +| %p | Returns pointer to supplied structure (table/function) | \`0xf20a8968\` | +| %q | Formats a string between double quotes, using escape sequences when necessary to ensure that it can safely be read back by the Lua interpreter | \`"test\\1\\2test"\` |`, + `--[[ +The following specifiers are exclusive to LuaJIT: + +| Format | Description | Example of the output | +| ------ | ----------- | --------------------- | +| %p | Returns pointer to supplied structure (table/function) | \`0xf20a8968\` | +| %q | Formats a string between double quotes, using escape sequences when necessary to ensure that it can safely be read back by the Lua interpreter | \`"test\\1\\2test"\` | +--]]`, + ], + ])('should put a comment before each line', (input, expected, skipFirstLine = false) => { + expect(wrapInComment(input, skipFirstLine)).toBe(expected); }); }); describe('safeFileName', () => { it.each([ - ['hello world','hello world'], + ['hello world', 'hello world'], ['hello:World', 'hello_World'], ['hello:World', 'hello-World', '-'], ['hello:World', 'helloWorld', ''], ['hello:World', 'hello World', ' '], - ])('should make a string "%s" safe ("%s") for use as a file name', (input: string, expected: string, replacement: string|undefined = undefined) => { + ])('should make a string "%s" safe ("%s") for use as a file name', (input: string, expected: string, replacement: string | undefined = undefined) => { if (replacement !== undefined) expect(safeFileName(input, replacement)).toBe(expected); else expect(safeFileName(input)).toBe(expected); }); }); + +describe('unindentText', () => { + it.each([ + ['hello\nworld', 'hello\nworld'], + [' hello\n world', 'hello\nworld'], + [' hello\n world', 'hello\nworld'], + ['\thello\n\tworld', 'hello\nworld'], + ['\thello\n\t world', 'hello\n world'], + ['\t\thello\n\t\tworld', 'hello\nworld'], + ])('should unindent text by the given amount', (input, expected) => { + expect(unindentText(input)).toBe(expected); + }); +}); diff --git a/src/api-writer/glua-api-writer.ts b/src/api-writer/glua-api-writer.ts index 7f2804e..c90b589 100644 --- a/src/api-writer/glua-api-writer.ts +++ b/src/api-writer/glua-api-writer.ts @@ -1,5 +1,5 @@ import { ClassFunction, Enum, Function, HookFunction, LibraryFunction, TypePage, Panel, PanelFunction, Realm, Struct, WikiPage, isPanel, FunctionArgument, FunctionCallback } from '../scrapers/wiki-page-markup-scraper.js'; -import { escapeSingleQuotes, indentText, putCommentBeforeEachLine, removeNewlines, safeFileName, toLowerCamelCase } from '../utils/string.js'; +import { escapeSingleQuotes, indentText, wrapInComment, removeNewlines, safeFileName, toLowerCamelCase } from '../utils/string.js'; import { isClassFunction, isHookFunction, @@ -117,9 +117,9 @@ export class GluaApiWriter { api += this.pageOverrides.get(classOverride)!.replace(/\n$/g, '') + '\n\n'; } else { if (realm) { - api += `---${this.formatRealm(realm)} ${description ? `${putCommentBeforeEachLine(description)}\n` : ''}\n`; + api += `---${this.formatRealm(realm)} ${description ? `${wrapInComment(description)}\n` : ''}\n`; } else { - api += description ? `${putCommentBeforeEachLine(description, false)}\n` : ''; + api += description ? `${wrapInComment(description, false)}\n` : ''; } if (url) { @@ -166,7 +166,7 @@ export class GluaApiWriter { if (!this.writtenLibraryGlobals.has(page.name)) { let api = ''; - api += page.description ? `${putCommentBeforeEachLine(page.description.trim(), false)}\n` : ''; + api += page.description ? `${wrapInComment(page.description, false)}\n` : ''; if (page.deprecated) api += `---@deprecated ${removeNewlines(page.deprecated)}\n`; @@ -244,7 +244,7 @@ export class GluaApiWriter { api += `---@deprecated ${removeNewlines(_enum.deprecated)}\n`; if (isContainedInTable) { - api += `---${this.formatRealm(_enum.realm)} ${_enum.description ? `${putCommentBeforeEachLine(_enum.description.trim())}` : ''}\n`; + api += `---${this.formatRealm(_enum.realm)} ${_enum.description ? `${wrapInComment(_enum.description)}` : ''}\n`; api += `---@enum ${_enum.name}\n`; api += `${_enum.name} = {\n`; } @@ -265,12 +265,12 @@ export class GluaApiWriter { key = key.split('.')[1]; if (item.description?.trim()) { - api += `${indentText(putCommentBeforeEachLine(item.description.trim(), false), 2)}\n`; + api += `${indentText(wrapInComment(item.description, false), 2)}\n`; } api += ` ${key} = ${item.value},\n`; } else { - api += item.description ? `${putCommentBeforeEachLine(item.description.trim(), false)}\n` : ''; + api += item.description ? `${wrapInComment(item.description, false)}\n` : ''; if (item.deprecated) api += `---@deprecated ${removeNewlines(item.deprecated)}\n`; api += `${key} = ${item.value}\n`; @@ -287,7 +287,7 @@ export class GluaApiWriter { // Until LuaLS supports global enumerations (https://github.com/LuaLS/lua-language-server/issues/2721) we // will use @alias as a workaround. // LuaLS doesn't nicely display annotations for aliasses, hence this is commented - //api += `\n---${this.formatRealm(_enum.realm)} ${_enum.description ? `${putCommentBeforeEachLine(_enum.description.trim())}` : ''}\n`; + //api += `\n---${this.formatRealm(_enum.realm)} ${_enum.description ? `${wrapInComment(_enum.description)}` : ''}\n`; api += `\n---@alias ${_enum.name}\n`; for (const item of _enum.items) { @@ -319,9 +319,9 @@ export class GluaApiWriter { if (field.deprecated) api += `---@deprecated ${removeNewlines(field.deprecated)}\n`; - api += `---${putCommentBeforeEachLine(field.description.trim())}\n`; + api += `---${wrapInComment(field.description)}\n`; - const type = this.transformType(field.type, field.callback); + const type = GluaApiWriter.transformType(field.type, field.callback); api += `---@type ${type}\n`; api += `${struct.name}.${GluaApiWriter.safeName(field.name)} = ${field.default ? this.writeType(type, field.default) : 'nil'}\n\n`; } @@ -376,7 +376,7 @@ export class GluaApiWriter { }); } - private transformType(type: string, callback?: FunctionCallback) { + public static transformType(type: string, callback?: FunctionCallback) { if (type === 'vararg') return 'any'; @@ -384,11 +384,19 @@ export class GluaApiWriter { if (type === 'function' && callback) { let callbackString = `fun(`; - for (const arg of callback.arguments || []) { - if (!arg.name) arg.name = arg.type; - if (arg.type === 'vararg') arg.name = '...'; + const callbackArgsLength = callback.arguments?.length || 0; - callbackString += `${GluaApiWriter.safeName(arg.name)}: ${this.transformType(arg.type)}${arg.default !== undefined ? `?` : ''}, `; + for (let i = 0; i < callbackArgsLength; i++) { + const arg = callback.arguments![i]; + + if (!arg.name) { + arg.name = `arg${i}`; + } + + if (arg.type === 'vararg') + arg.name = '...'; + + callbackString += `${GluaApiWriter.safeName(arg.name)}: ${GluaApiWriter.transformType(arg.type)}${arg.default !== undefined ? `?` : ''}, `; } // Remove trailing comma and space @@ -401,9 +409,17 @@ export class GluaApiWriter { callbackString += ':('; } - for (const ret of callback.returns || []) { - if (!ret.name) ret.name = ret.type; - if (ret.type === 'vararg') ret.name = '...'; + const callbackReturnsLength = callback.returns?.length || 0; + + for (let i = 0; i < callbackReturnsLength; i++) { + const ret = callback.returns![i]; + + if (!ret.name) { + ret.name = `ret${i}`; + } + + if (ret.type === 'vararg') + ret.name = '...'; callbackString += `${ret.name}: ${this.transformType(ret.type)}${ret.default !== undefined ? `?` : ''}, `; } @@ -424,8 +440,9 @@ export class GluaApiWriter { if (!innerType) throw new Error(`Invalid table type: ${type}`); return `${innerType}[]`; - } else if (type.startsWith('table{')) { + } else if (type.startsWith('table{') || type.startsWith('Panel{')) { // Convert `table{ToScreenData}` structures to `ToScreenData` class for LuaLS + // Also converts `Panel{DVScrollBar}` to `DVScrollBar` class for LuaLS let innerType = type.match(/{([^}]+)}/)?.[1]; if (!innerType) throw new Error(`Invalid table type: ${type}`); @@ -464,7 +481,7 @@ export class GluaApiWriter { } private writeFunctionLuaDocComment(func: Function, args: FunctionArgument[] | undefined, realm: Realm) { - let luaDocComment = `---${this.formatRealm(realm)} ${putCommentBeforeEachLine(func.description!.trim())}\n`; + let luaDocComment = `---${this.formatRealm(realm)} ${wrapInComment(func.description!)}\n`; luaDocComment += `---\n---[View wiki](${func.url})\n`; if (args) { @@ -485,23 +502,23 @@ export class GluaApiWriter { types.push(arg.altType); } - let typesString = types.map(type => this.transformType(type, arg.callback)) + let typesString = types.map(type => GluaApiWriter.transformType(type, arg.callback)) .join('|'); - luaDocComment += `---@param ${GluaApiWriter.safeName(arg.name)}${arg.default !== undefined ? `?` : ''} ${typesString} ${putCommentBeforeEachLine(arg.description!.trim())}\n`; + luaDocComment += `---@param ${GluaApiWriter.safeName(arg.name)}${arg.default !== undefined ? `?` : ''} ${typesString} ${wrapInComment(arg.description!)}\n`; }); } if (func.returns) { func.returns.forEach(ret => { - const description = putCommentBeforeEachLine(ret.description!.trim()); + const description = wrapInComment(ret.description!); luaDocComment += `---@return `; if (ret.type === 'vararg') luaDocComment += 'any ...'; else - luaDocComment += `${this.transformType(ret.type, ret.callback)}`; + luaDocComment += `${GluaApiWriter.transformType(ret.type, ret.callback)}`; luaDocComment += ` # ${description}\n`; }); diff --git a/src/scrapers/wiki-page-markup-scraper.ts b/src/scrapers/wiki-page-markup-scraper.ts index b7d0639..3af6804 100644 --- a/src/scrapers/wiki-page-markup-scraper.ts +++ b/src/scrapers/wiki-page-markup-scraper.ts @@ -142,7 +142,7 @@ export function isClass(page: WikiPage): page is TypePage { // Handle things like tags, , etc. const classBlocks = ["internal", "note", "warning"]; -function markdownifyDescription($: CheerioAPI, e: Cheerio): string { +export function markdownifyDescription($: CheerioAPI, e: Cheerio): string { // => markdown []() for (const page of $(e).find('page')) { const $p = $(page); @@ -165,6 +165,7 @@ function markdownifyDescription($: CheerioAPI, e: Cheerio): string { // Fixup []() that use local URLs. This is not exclusive to the result of conversions description = description.replace(/\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/g, function (match, text, url) { if (url.indexOf("://") == -1) { + url = url.replace(/ /g, "_"); return `[${text}](https://wiki.facepunch.com/gmod/${url})`; } diff --git a/src/utils/string.ts b/src/utils/string.ts index 6c35aba..390ba98 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -21,12 +21,7 @@ export function removeNewlines(text: string) { /** * Puts a comment before each line in a string */ -export function putCommentBeforeEachLine(text: string, skipLineOne: boolean = true) { - // Remove duplicate consequitive new lines. - while (text.match(/\r?\n\r?\n\r?\n/g)) { - text = text.replace(/\r?\n\r?\n\r?\n/g, "\n\n"); - } - +function putCommentBeforeEachLine(text: string, skipLineOne: boolean = true) { return text.split(/\r?\n/g).map((line, index) => { if (index === 0 && skipLineOne) return line; @@ -37,6 +32,52 @@ export function putCommentBeforeEachLine(text: string, skipLineOne: boolean = tr }).join('\n'); } +/** + * Puts every following line in a long comment. We do this so any markdown + * inside the comment doesn't break. Like markdown tables only work in + * long comments. + */ +export function wrapInComment(text: string, skipLineOne: boolean = true) { + // Trim any leading and trailing line breaks, or lines with only white spaces. + text = text.replace(/^\s*\r?\n|\r?\n\s*$/g, ''); + + // Remove duplicate consequitive new lines. + while (text.match(/\r?\n\r?\n\r?\n/g)) { + text = text.replace(/\r?\n\r?\n\r?\n/g, "\n\n"); + } + + text = unindentText(text); + + // If the text contains no markdown tables, we use the old method + // of putting a comment before each line. + if (!text.match(/^\s*\|.*\|\s*\n^\s*\|(?:\s*[:\-]+[-| :]*).*$/gm)) { + return putCommentBeforeEachLine(text, skipLineOne); + } + + const lines = text.split(/\r?\n/g); + let output = ''; + let i = 0; + + if (skipLineOne) { + if (lines.length <= 1) { + return text; + } + + output = `${lines[0]}\n--[[\n`; + i = 1; + } else { + output = '--[[\n'; + } + + for (; i < lines.length; i++) { + output += `${lines[i]}\n`; + } + + output += '--]]'; + + return output; +} + /** * Makes a string safe for use as a file name * @@ -68,3 +109,22 @@ export function escapeSingleQuotes(str: string) { export function indentText(text: string, indent: number) { return text.split('\n').map(line => ' '.repeat(indent) + line).join('\n'); } + +/** + * Unindents all lines in the text using the given indent amount or + * by the first line's indent amount. + * @param text The text to unindent + * @param indent The amount of spaces to unindent by + * @returns The unindented text + */ +export function unindentText(text: string, indent: number = 0) { + const lines = text.split('\n'); + + if (lines.length === 0) + return text; + + const firstLineIndent = lines[0].search(/\S/); + const unindentAmount = indent > 0 ? indent : firstLineIndent; + + return lines.map(line => line.slice(unindentAmount)).join('\n'); +}