mirror of
				https://github.com/theoludwig/markdownlint-rule-relative-links.git
				synced 2025-11-01 00:49:07 +01:00 
			
		
		
		
	feat: validate relative links fragments
Similar to https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md Fixes #2 BREAKING CHANGE: Validate links fragments in relative links
This commit is contained in:
		
							
								
								
									
										15
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -9,6 +9,9 @@
 | 
			
		||||
      "version": "0.0.0-development",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "markdown-it": "13.0.1"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@commitlint/cli": "17.6.5",
 | 
			
		||||
        "@commitlint/config-conventional": "17.6.5",
 | 
			
		||||
@@ -1619,8 +1622,7 @@
 | 
			
		||||
    "node_modules/argparse": {
 | 
			
		||||
      "version": "2.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/argv-formatter": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
@@ -2447,7 +2449,6 @@
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.12"
 | 
			
		||||
      },
 | 
			
		||||
@@ -4372,7 +4373,6 @@
 | 
			
		||||
      "version": "4.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "uc.micro": "^1.0.1"
 | 
			
		||||
      }
 | 
			
		||||
@@ -4872,7 +4872,6 @@
 | 
			
		||||
      "version": "13.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "argparse": "^2.0.1",
 | 
			
		||||
        "entities": "~3.0.1",
 | 
			
		||||
@@ -5023,8 +5022,7 @@
 | 
			
		||||
    "node_modules/mdurl": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/meow": {
 | 
			
		||||
      "version": "8.1.2",
 | 
			
		||||
@@ -10471,8 +10469,7 @@
 | 
			
		||||
    "node_modules/uc.micro": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/uglify-js": {
 | 
			
		||||
      "version": "3.17.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,9 @@
 | 
			
		||||
    "prepublishOnly": "pinst --disable",
 | 
			
		||||
    "postpublish": "pinst --enable"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "markdown-it": "13.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "17.6.5",
 | 
			
		||||
    "@commitlint/config-conventional": "17.6.5",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								src/index.js
									
									
									
									
									
								
							@@ -3,42 +3,12 @@
 | 
			
		||||
const { pathToFileURL } = require('node:url')
 | 
			
		||||
const fs = require('node:fs')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calls the provided function for each matching token.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} params RuleParams instance.
 | 
			
		||||
 * @param {string} type Token type identifier.
 | 
			
		||||
 * @param {Function} handler Callback function.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
const filterTokens = (params, type, handler) => {
 | 
			
		||||
  for (const token of params.tokens) {
 | 
			
		||||
    if (token.type === type) {
 | 
			
		||||
      handler(token)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adds a generic error object via the onError callback.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} onError RuleOnError instance.
 | 
			
		||||
 * @param {number} lineNumber Line number.
 | 
			
		||||
 * @param {string} [detail] Error details.
 | 
			
		||||
 * @param {string} [context] Error context.
 | 
			
		||||
 * @param {number[]} [range] Column and length of error.
 | 
			
		||||
 * @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
const addError = (onError, lineNumber, detail, context, range, fixInfo) => {
 | 
			
		||||
  onError({
 | 
			
		||||
    lineNumber,
 | 
			
		||||
    detail,
 | 
			
		||||
    context,
 | 
			
		||||
    range,
 | 
			
		||||
    fixInfo
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
const {
 | 
			
		||||
  filterTokens,
 | 
			
		||||
  addError,
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings
 | 
			
		||||
} = require('./utils.js')
 | 
			
		||||
 | 
			
		||||
const customRule = {
 | 
			
		||||
  names: ['relative-links'],
 | 
			
		||||
@@ -70,12 +40,37 @@ const customRule = {
 | 
			
		||||
 | 
			
		||||
        if (hrefSrc != null) {
 | 
			
		||||
          const url = new URL(hrefSrc, pathToFileURL(params.name))
 | 
			
		||||
          url.hash = ''
 | 
			
		||||
          const isRelative =
 | 
			
		||||
            url.protocol === 'file:' && !hrefSrc.startsWith('/')
 | 
			
		||||
          if (isRelative && !fs.existsSync(url)) {
 | 
			
		||||
            const detail = `Link "${hrefSrc}" is dead`
 | 
			
		||||
          if (isRelative) {
 | 
			
		||||
            const detail = `Link "${hrefSrc}" is not valid`
 | 
			
		||||
 | 
			
		||||
            if (!fs.existsSync(url)) {
 | 
			
		||||
              addError(onError, lineNumber, detail)
 | 
			
		||||
              return
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (type === 'link_open' && url.hash !== '') {
 | 
			
		||||
              const fileContent = fs.readFileSync(url, { encoding: 'utf8' })
 | 
			
		||||
              const headings = getMarkdownHeadings(fileContent)
 | 
			
		||||
 | 
			
		||||
              /** @type {Map<string, number>} */
 | 
			
		||||
              const fragments = new Map()
 | 
			
		||||
 | 
			
		||||
              const headingsHTMLFragments = headings.map((heading) => {
 | 
			
		||||
                const fragment = convertHeadingToHTMLFragment(heading)
 | 
			
		||||
                const count = fragments.get(fragment) ?? 0
 | 
			
		||||
                fragments.set(fragment, count + 1)
 | 
			
		||||
                if (count !== 0) {
 | 
			
		||||
                  return `${fragment}-${count}`
 | 
			
		||||
                }
 | 
			
		||||
                return fragment
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
              if (!headingsHTMLFragments.includes(url.hash)) {
 | 
			
		||||
                addError(onError, lineNumber, detail)
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										120
									
								
								src/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/utils.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
			
		||||
const MarkdownIt = require('markdown-it')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calls the provided function for each matching token.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} params RuleParams instance.
 | 
			
		||||
 * @param {string} type Token type identifier.
 | 
			
		||||
 * @param {Function} handler Callback function.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
const filterTokens = (params, type, handler) => {
 | 
			
		||||
  for (const token of params.tokens) {
 | 
			
		||||
    if (token.type === type) {
 | 
			
		||||
      handler(token)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adds a generic error object via the onError callback.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} onError RuleOnError instance.
 | 
			
		||||
 * @param {number} lineNumber Line number.
 | 
			
		||||
 * @param {string} [detail] Error details.
 | 
			
		||||
 * @param {string} [context] Error context.
 | 
			
		||||
 * @param {number[]} [range] Column and length of error.
 | 
			
		||||
 * @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
 | 
			
		||||
 * @returns {void}
 | 
			
		||||
 */
 | 
			
		||||
const addError = (onError, lineNumber, detail, context, range, fixInfo) => {
 | 
			
		||||
  onError({
 | 
			
		||||
    lineNumber,
 | 
			
		||||
    detail,
 | 
			
		||||
    context,
 | 
			
		||||
    range,
 | 
			
		||||
    fixInfo
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts a Markdown heading into an HTML fragment according to the rules
 | 
			
		||||
 * used by GitHub.
 | 
			
		||||
 *
 | 
			
		||||
 * Taken from <https://github.com/DavidAnson/markdownlint/blob/d01180ec5a014083ee9d574b693a8d7fbc1e566d/lib/md051.js#L19>
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} inlineText Inline token for heading.
 | 
			
		||||
 * @returns {string} Fragment string for heading.
 | 
			
		||||
 */
 | 
			
		||||
const convertHeadingToHTMLFragment = (inlineText) => {
 | 
			
		||||
  return (
 | 
			
		||||
    '#' +
 | 
			
		||||
    encodeURIComponent(
 | 
			
		||||
      inlineText
 | 
			
		||||
        .toLowerCase()
 | 
			
		||||
        // RegExp source with Ruby's \p{Word} expanded into its General Categories
 | 
			
		||||
        // eslint-disable-next-line max-len
 | 
			
		||||
        // https://github.com/gjtorikian/html-pipeline/blob/main/lib/html/pipeline/toc_filter.rb
 | 
			
		||||
        // https://ruby-doc.org/core-3.0.2/Regexp.html
 | 
			
		||||
        .replace(
 | 
			
		||||
          /[^\p{Letter}\p{Mark}\p{Number}\p{Connector_Punctuation}\- ]/gu,
 | 
			
		||||
          ''
 | 
			
		||||
        )
 | 
			
		||||
        .replace(/ /gu, '-')
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headingTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
 | 
			
		||||
const ignoredTokens = new Set(['heading_open', 'heading_close'])
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the headings from a Markdown string.
 | 
			
		||||
 * @param {string} content
 | 
			
		||||
 * @returns {string[]}
 | 
			
		||||
 */
 | 
			
		||||
const getMarkdownHeadings = (content) => {
 | 
			
		||||
  const markdownIt = new MarkdownIt({ html: true })
 | 
			
		||||
  const tokens = markdownIt.parse(content, {})
 | 
			
		||||
 | 
			
		||||
  /** @type {string[]} */
 | 
			
		||||
  const headings = []
 | 
			
		||||
 | 
			
		||||
  /** @type {string | null} */
 | 
			
		||||
  let headingToken = null
 | 
			
		||||
 | 
			
		||||
  for (const token of tokens) {
 | 
			
		||||
    if (headingTags.has(token.tag)) {
 | 
			
		||||
      if (token.type === 'heading_open') {
 | 
			
		||||
        headingToken = token.markup
 | 
			
		||||
      } else if (token.type === 'heading_close') {
 | 
			
		||||
        headingToken = null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (ignoredTokens.has(token.type)) {
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (headingToken === null) {
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    headings.push(
 | 
			
		||||
      `${token.children
 | 
			
		||||
        .map((token) => {
 | 
			
		||||
          return token.content
 | 
			
		||||
        })
 | 
			
		||||
        .join('')}`
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return headings
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  filterTokens,
 | 
			
		||||
  addError,
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings
 | 
			
		||||
}
 | 
			
		||||
@@ -5,16 +5,17 @@ const { markdownlint } = require('markdownlint').promises
 | 
			
		||||
 | 
			
		||||
const relativeLinks = require('../src/index.js')
 | 
			
		||||
 | 
			
		||||
test('ensure we validate correctly', async () => {
 | 
			
		||||
test('ensure the rule validate correctly', async () => {
 | 
			
		||||
  const lintResults = await markdownlint({
 | 
			
		||||
    files: ['test/fixtures/Valid.md', 'test/fixtures/Invalid.md'],
 | 
			
		||||
    config: {
 | 
			
		||||
      default: false,
 | 
			
		||||
      'relative-links': true
 | 
			
		||||
    },
 | 
			
		||||
    customRules: [relativeLinks]
 | 
			
		||||
  })
 | 
			
		||||
  assert.equal(lintResults['test/fixtures/Valid.md'].length, 0)
 | 
			
		||||
  assert.equal(lintResults['test/fixtures/Invalid.md'].length, 2)
 | 
			
		||||
  assert.equal(lintResults['test/fixtures/Invalid.md'].length, 3)
 | 
			
		||||
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    lintResults['test/fixtures/Invalid.md'][0]?.ruleDescription,
 | 
			
		||||
@@ -22,7 +23,7 @@ test('ensure we validate correctly', async () => {
 | 
			
		||||
  )
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    lintResults['test/fixtures/Invalid.md'][0]?.errorDetail,
 | 
			
		||||
    'Link "./basic.test.js" is dead'
 | 
			
		||||
    'Link "./basic.test.js" is not valid'
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  assert.equal(
 | 
			
		||||
@@ -31,6 +32,15 @@ test('ensure we validate correctly', async () => {
 | 
			
		||||
  )
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    lintResults['test/fixtures/Invalid.md'][1]?.errorDetail,
 | 
			
		||||
    'Link "../image.png" is dead'
 | 
			
		||||
    'Link "../image.png" is not valid'
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    lintResults['test/fixtures/Invalid.md'][2]?.ruleDescription,
 | 
			
		||||
    'Relative links should be valid'
 | 
			
		||||
  )
 | 
			
		||||
  assert.equal(
 | 
			
		||||
    lintResults['test/fixtures/Invalid.md'][2]?.errorDetail,
 | 
			
		||||
    'Link "./Valid.md#not-existing-heading" is not valid'
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								test/fixtures/Invalid.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								test/fixtures/Invalid.md
									
									
									
									
										vendored
									
									
								
							@@ -3,3 +3,17 @@
 | 
			
		||||
[basic.js](./basic.test.js)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
[Link fragment](./Valid.md#not-existing-heading)
 | 
			
		||||
 | 
			
		||||
## Existing Heading
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
 | 
			
		||||
### Repeated Heading
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								test/fixtures/Valid.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								test/fixtures/Valid.md
									
									
									
									
										vendored
									
									
								
							@@ -11,3 +11,11 @@
 | 
			
		||||
[External https link 2](https:./external.https)
 | 
			
		||||
 | 
			
		||||
[External ftp link](ftp:./external.ftp)
 | 
			
		||||
 | 
			
		||||
[Link fragment](./Invalid.md#existing-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 0](./Invalid.md#repeated-heading)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 1](./Invalid.md#repeated-heading-1)
 | 
			
		||||
 | 
			
		||||
[Link fragment Repeated 2](./Invalid.md#repeated-heading-2)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								test/utils.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								test/utils.test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
const test = require('node:test')
 | 
			
		||||
const assert = require('node:assert/strict')
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  convertHeadingToHTMLFragment,
 | 
			
		||||
  getMarkdownHeadings
 | 
			
		||||
} = require('../src/utils.js')
 | 
			
		||||
 | 
			
		||||
test('utils', async (t) => {
 | 
			
		||||
  await t.test('convertHeadingToHTMLFragment', async () => {
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      convertHeadingToHTMLFragment('Valid Fragments'),
 | 
			
		||||
      '#valid-fragments'
 | 
			
		||||
    )
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      convertHeadingToHTMLFragment('Valid Heading With Underscores _'),
 | 
			
		||||
      '#valid-heading-with-underscores-_'
 | 
			
		||||
    )
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      convertHeadingToHTMLFragment(
 | 
			
		||||
        `Valid Heading With Quotes ' And Double Quotes "`
 | 
			
		||||
      ),
 | 
			
		||||
      '#valid-heading-with-quotes--and-double-quotes-'
 | 
			
		||||
    )
 | 
			
		||||
    assert.strictEqual(
 | 
			
		||||
      convertHeadingToHTMLFragment('🚀 Valid Heading With Emoji'),
 | 
			
		||||
      '#-valid-heading-with-emoji'
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await t.test('getMarkdownHeadings', async () => {
 | 
			
		||||
    assert.deepStrictEqual(
 | 
			
		||||
      getMarkdownHeadings('# Hello\n\n## World\n\n## Hello, world!\n'),
 | 
			
		||||
      ['Hello', 'World', 'Hello, world!']
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user