diff --git a/src/resolvers/lintPage.ts b/src/resolvers/lintPage.ts index f795bb2e..a98ed0b7 100644 --- a/src/resolvers/lintPage.ts +++ b/src/resolvers/lintPage.ts @@ -1,15 +1,26 @@ import {dirname, relative, resolve} from 'path'; +import {load} from 'js-yaml'; import log from '@diplodoc/transform/lib/log'; import { LintMarkdownFunctionOptions, PluginOptions, default as yfmlint, } from '@diplodoc/transform/lib/yfmlint'; +import {isLocalUrl} from '@diplodoc/transform/lib/utils'; +import {getLogLevel} from '@diplodoc/transform/lib/yfmlint/utils'; +import {LINK_KEYS} from '@diplodoc/client/ssr'; + import {readFileSync} from 'fs'; import {bold} from 'chalk'; import {ArgvService, PluginService} from '../services'; -import {getVarsPerFile, getVarsPerRelativeFile} from '../utils'; +import { + checkPathExists, + findAllValuesByKeys, + getLinksWithExtension, + getVarsPerFile, + getVarsPerRelativeFile, +} from '../utils'; import {liquidMd2Html} from './md2html'; import {liquidMd2Md} from './md2md'; @@ -20,6 +31,7 @@ interface FileTransformOptions { const FileLinter: Record = { '.md': MdFileLinter, + '.yaml': YamlFileLinter, }; export interface ResolverLintOptions { @@ -53,6 +65,29 @@ export function lintPage(options: ResolverLintOptions) { } } +function YamlFileLinter(content: string, lintOptions: FileTransformOptions): void { + const {input, lintConfig} = ArgvService.getConfig(); + const {path: filePath} = lintOptions; + const currentFilePath: string = resolve(input, filePath); + + const logLevel = getLogLevel({ + logLevelsConfig: lintConfig['log-levels'], + ruleNames: ['YAML001'], + defaultLevel: log.LogLevels.ERROR, + }); + + const contentLinks = findAllValuesByKeys(load(content), LINK_KEYS); + const localLinks = contentLinks.filter( + (link) => getLinksWithExtension(link) && isLocalUrl(link), + ); + + return localLinks.forEach( + (link) => + checkPathExists(link, currentFilePath) || + log[logLevel](`Link is unreachable: ${bold(link)} in ${bold(currentFilePath)}`), + ); +} + function MdFileLinter(content: string, lintOptions: FileTransformOptions): void { const {input, lintConfig, disableLiquid, outputFormat, ...options} = ArgvService.getConfig(); const {path: filePath} = lintOptions; diff --git a/src/utils/markup.ts b/src/utils/markup.ts index d431acda..a5b5be34 100644 --- a/src/utils/markup.ts +++ b/src/utils/markup.ts @@ -1,5 +1,6 @@ import {join} from 'path'; import {platform} from 'process'; +import {flatMapDeep, isArray, isObject, isString} from 'lodash'; import {CUSTOM_STYLE, Platforms, RTL_LANGS} from '../constants'; import {LeadingPage, Resources, SinglePageResult, TextItems, VarsMetadata} from '../models'; @@ -8,6 +9,7 @@ import {preprocessPageHtmlForSinglePage} from './singlePage'; import {DocInnerProps, DocPageData, render} from '@diplodoc/client/ssr'; import manifest from '@diplodoc/client/manifest'; +import {isFileExists, resolveRelativePath} from '@diplodoc/transform/lib/utilsFS'; import {escape} from 'html-escaper'; @@ -168,3 +170,34 @@ export function joinSinglePageResults( export function replaceDoubleToSingleQuotes(str: string): string { return str.replace(/"/g, "'"); } + +export function findAllValuesByKeys(obj, keysToFind) { + return flatMapDeep(obj, (value, key) => { + if ( + keysToFind.includes(key) && + (isString(value) || (isArray(value) && value.every(isString))) + ) { + return [value]; + } + + if (isObject(value)) { + return findAllValuesByKeys(value, keysToFind); + } + + return []; + }); +} + +export function getLinksWithExtension(link) { + const oneLineWithExtension = new RegExp( + /^\S.*\.(md|html|yaml|svg|png|gif|jpg|jpeg|bmp|webp|ico)$/gm, + ); + + return oneLineWithExtension.test(link); +} + +export function checkPathExists(path, parentFilePath) { + const includePath = resolveRelativePath(parentFilePath, path); + + return isFileExists(includePath); +} diff --git a/tests/units/services/utils.test.ts b/tests/units/services/utils.test.ts index 4e401337..55e05047 100644 --- a/tests/units/services/utils.test.ts +++ b/tests/units/services/utils.test.ts @@ -2,6 +2,7 @@ import {filterTextItems, filterFiles, firstFilterTextItems, liquidField} from "s import {Lang} from "../../../src/constants"; import {ArgvService} from "../../../src/services"; import {YfmArgv} from "models"; +import {findAllValuesByKeys} from "utils"; const combinedVars = { lang: Lang.EN, @@ -240,3 +241,37 @@ describe('liquidField', () => { expect(result).toBe(`{% if type == 'a' %}{{a}}{% else %}{{b}}{% endif %}`); }); }); + +describe('findAllValuesByKeys', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should return an empty array if no values match the keys', () => { + const obj = {a: 1, b: 2}; + const keysToFind = ['c', 'd']; + const result = findAllValuesByKeys(obj, keysToFind); + expect(result).toEqual([]); + }); + + test('should return an array of string values that match the keys', () => { + const obj = {a: 'foo', b: 'bar', c: 'baz'}; + const keysToFind = ['a', 'b']; + const result = findAllValuesByKeys(obj, keysToFind); + expect(result).toEqual(['foo', 'bar']); + }); + + test('should return an array of string values from nested objects that match the keys', () => { + const obj = {a: {b: 'foo'}, c: [{d: 'bar'}, {e: 'baz'}]}; + const keysToFind = ['b', 'd', 'e']; + const result = findAllValuesByKeys(obj, keysToFind); + expect(result).toEqual(['foo', 'bar', 'baz']); + }); + + test('should return an array of string values from arrays that match the keys', () => { + const obj = {a: ['foo', 'bar'], b: [1, 2]}; + const keysToFind = ['a']; + const result = findAllValuesByKeys(obj, keysToFind); + expect(result).toEqual(['foo', 'bar']); + }); +});