diff --git a/package.json b/package.json index 1e4565e2c..c96392de1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md", "buildDoc": "./packages/tools/build-all.sh", "buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/", + "buildPluginRepo": "node packages/tools/build-plugin-repository.js", "buildTranslations": "npm run tsc && node packages/tools/build-translation.js", "buildTranslationsNoTsc": "node packages/tools/build-translation.js", "buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc", diff --git a/packages/lib/markdownUtils.ts b/packages/lib/markdownUtils.ts index 885cbfd7d..e0959cff6 100644 --- a/packages/lib/markdownUtils.ts +++ b/packages/lib/markdownUtils.ts @@ -30,6 +30,17 @@ const markdownUtils = { return url; }, + escapeTableCell(text: string) { + // Disable HTML code + text = text.replace(//g, '>'); + // Table cells can't contain new lines so replace with
+ text = text.replace(/\n/g, '
'); + // "|" is a reserved characters that should be escaped + text = text.replace(/\|/g, '\\|'); + return text; + }, + unescapeLinkUrl(url: string) { url = url.replace(/%28/g, '('); url = url.replace(/%29/g, ')'); @@ -117,8 +128,8 @@ const markdownUtils = { const rowMd = []; for (let j = 0; j < headers.length; j++) { const h = headers[j]; - const value = h.filter ? h.filter(row[h.name]) : row[h.name]; - rowMd.push(stringPadding(value, 3, ' ', stringPadding.RIGHT)); + const valueMd = markdownUtils.escapeTableCell(h.filter ? h.filter(row[h.name]) : row[h.name]); + rowMd.push(stringPadding(valueMd, 3, ' ', stringPadding.RIGHT)); } output.push(rowMd.join(' | ')); } diff --git a/packages/tools/build-plugin-repository.ts b/packages/tools/build-plugin-repository.ts index 4ab7ebde3..c57e6e212 100644 --- a/packages/tools/build-plugin-repository.ts +++ b/packages/tools/build-plugin-repository.ts @@ -11,12 +11,23 @@ interface NpmPackage { date: Date; } +function stripOffPackageOrg(name: string): string { + const n = name.split('/'); + if (n[0][0] === '@') n.splice(0, 1); + return n.join('/'); +} + +function isJoplinPluginPackage(pack: any): boolean { + if (!pack.keywords || !pack.keywords.includes('joplin-plugin')) return false; + if (stripOffPackageOrg(pack.name).indexOf('joplin-plugin') !== 0) return false; + return true; +} + function pluginInfoFromSearchResults(results: any[]): NpmPackage[] { const output: NpmPackage[] = []; for (const r of results) { - if (r.name.indexOf('joplin-plugin') !== 0) continue; - if (!r.keywords || !r.keywords.includes('joplin-plugin')) continue; + if (!isJoplinPluginPackage(r)) continue; output.push({ name: r.name, @@ -48,7 +59,14 @@ async function readJsonFile(manifestPath: string, defaultValue: any = null): Pro return JSON.parse(content); } -async function extractPluginFilesFromPackage(originalPluginManifests: any, workDir: string, packageName: string, destDir: string): Promise { +function caseInsensitiveFindManifest(manifests: any, manifestId: string): any { + for (const id of Object.keys(manifests)) { + if (id.toLowerCase() === manifestId.toLowerCase()) return manifests[id]; + } + return null; +} + +async function extractPluginFilesFromPackage(existingManifests: any, workDir: string, packageName: string, destDir: string): Promise { const previousDir = process.cwd(); process.chdir(workDir); @@ -76,7 +94,11 @@ async function extractPluginFilesFromPackage(originalPluginManifests: any, workD // package name, we skip it. Otherwise it would allow anyone to overwrite // someone else plugin just by using the same ID. So the first plugin with // this ID that was originally added is kept. - const originalManifest = originalPluginManifests[manifest.id]; + // + // We need case insensitive match because the filesystem might be case + // insensitive too. + const originalManifest = caseInsensitiveFindManifest(existingManifests, manifest.id); + if (originalManifest && originalManifest._npm_package_name !== packageName) { throw new Error(`Plugin "${manifest.id}" from npm package "${packageName}" has already been published under npm package "${originalManifest._npm_package_name}". Plugin from package "${packageName}" will not be imported.`); } @@ -145,6 +167,7 @@ async function main() { const repoDir = path.resolve(path.dirname(rootDir), 'joplin-plugins'); const tempDir = `${repoDir}/temp`; const pluginManifestsPath = path.resolve(repoDir, 'manifests.json'); + const obsoleteManifestsPath = path.resolve(repoDir, 'obsoletes.json'); const errorsPath = path.resolve(repoDir, 'errors.json'); await checkPluginRepository(repoDir); @@ -152,6 +175,11 @@ async function main() { await fs.mkdirp(tempDir); const originalPluginManifests = await readJsonFile(pluginManifestsPath, {}); + const obsoleteManifests = await readJsonFile(obsoleteManifestsPath, {}); + const existingManifests = { + ...originalPluginManifests, + ...obsoleteManifests, + }; const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json')).trim(); const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults)); @@ -172,8 +200,8 @@ async function main() { try { const packageName = npmPackage.name; const destDir = `${repoDir}/plugins/`; - const manifest = await extractPluginFilesFromPackage(originalPluginManifests, packageTempDir, packageName, destDir); - manifests[manifest.id] = manifest; + const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, packageName, destDir); + if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest; } catch (error) { console.error(error); errors.push(error);