mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-22 06:59:30 -06:00
docs: validate hashes exist when using {% url %} tag helper
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
const _ = require("lodash")
|
||||
const _ = require('lodash')
|
||||
const fs = require('hexo-fs')
|
||||
const url = require('url')
|
||||
const path = require('path')
|
||||
const cheerio = require('cheerio')
|
||||
const Promise = require('bluebird')
|
||||
const request = require('request-promise')
|
||||
const errors = require('request-promise/errors')
|
||||
|
||||
const startsWithHttpRe = /^http/
|
||||
const everythingAfterHashRe = /(#.+)/
|
||||
@@ -10,21 +14,54 @@ function isExternalHref (str) {
|
||||
return startsWithHttpRe.test(str)
|
||||
}
|
||||
|
||||
function stripHashes (str) {
|
||||
function stripHash (str) {
|
||||
return str.replace(everythingAfterHashRe, '')
|
||||
}
|
||||
|
||||
function extractHash (str) {
|
||||
const matches = everythingAfterHashRe.exec(str)
|
||||
|
||||
// return the hash match or empty string
|
||||
return (matches && matches[0]) || ''
|
||||
}
|
||||
|
||||
function validateExternalUrl (href) {
|
||||
function assertHashIsPresent (descriptor, source, hash, html) {
|
||||
// verify that the hash is present on this page
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
// hash starts with a '#'
|
||||
if (!$(hash).length) {
|
||||
const truncated = _.truncate(html, { length: 200 }) || '""'
|
||||
|
||||
// if we dont have a hash
|
||||
throw new Error(`Constructing {% url %} tag helper failed
|
||||
|
||||
> The source file was: ${source}
|
||||
|
||||
> You referenced a hash that does not exist at: ${descriptor}
|
||||
|
||||
> Expected to find an element matching the id: ${hash}
|
||||
|
||||
> The HTML response body was:
|
||||
|
||||
${truncated}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateExternalUrl (href, source) {
|
||||
const { hash } = url.parse(href)
|
||||
|
||||
return request(href)
|
||||
.promise()
|
||||
.return(href)
|
||||
.catch((err) => {
|
||||
.then((html) => {
|
||||
// bail if we dont have a hash
|
||||
if (!hash) {
|
||||
return
|
||||
}
|
||||
|
||||
assertHashIsPresent(href, source, hash, html)
|
||||
})
|
||||
.catch(errors.StatusCodeError, (err) => {
|
||||
err.message = `Request to: ${href} failed. (Status Code ${err.statusCode})`
|
||||
throw err
|
||||
})
|
||||
@@ -67,7 +104,7 @@ function normalizeNestedPaths (data) {
|
||||
function findFileBySource (sidebar, href) {
|
||||
const { expanded, flattened } = normalizeNestedPaths(sidebar)
|
||||
|
||||
href = stripHashes(href)
|
||||
href = stripHash(href)
|
||||
|
||||
function property () {
|
||||
// drill into the original sidebar object
|
||||
@@ -79,31 +116,59 @@ function findFileBySource (sidebar, href) {
|
||||
return flattened[href] || property()
|
||||
}
|
||||
|
||||
function findUrlByFile (sidebar, href) {
|
||||
function getLocalFile (sidebar, href) {
|
||||
// get the resolve path to the file
|
||||
// cypress-101 -> guides/core-concepts/cypress-101.html
|
||||
const pathToFile = findFileBySource(sidebar, href)
|
||||
|
||||
const hash = extractHash(href)
|
||||
|
||||
if (pathToFile) {
|
||||
// ensure this physically exists on disk
|
||||
// inside of './source' folder
|
||||
return fs.stat(
|
||||
return fs.readFile(
|
||||
path.resolve('source', pathToFile.replace('.html', '.md'))
|
||||
)
|
||||
.return(`/${pathToFile}${hash}`)
|
||||
).then((str) => {
|
||||
// return an array with
|
||||
// path to file, and str bytes
|
||||
return [pathToFile, str]
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error(`Could not find a valid doc file in the sidebar.yml for: ${href}`)
|
||||
return Promise.reject(
|
||||
new Error(`Could not find a valid doc file in the sidebar.yml for: ${href}`)
|
||||
)
|
||||
}
|
||||
|
||||
function validateAndGetUrl (sidebar, href) {
|
||||
function validateLocalFile (sidebar, href, source, render) {
|
||||
const hash = extractHash(href)
|
||||
|
||||
return getLocalFile(sidebar, stripHash(href))
|
||||
.spread((pathToFile, str) => {
|
||||
if (hash) {
|
||||
// if we have a hash then render
|
||||
// the markdown contents so we can
|
||||
// ensure it has the hash present!
|
||||
return render(str)
|
||||
.then((html) => {
|
||||
assertHashIsPresent(pathToFile, source, hash, html)
|
||||
|
||||
return pathToFile
|
||||
})
|
||||
}
|
||||
|
||||
return pathToFile
|
||||
})
|
||||
.then((pathToFile) => {
|
||||
return `/${pathToFile}${hash}`
|
||||
})
|
||||
}
|
||||
|
||||
function validateAndGetUrl (sidebar, href, source, render) {
|
||||
if (isExternalHref(href)) {
|
||||
return validateExternalUrl(href)
|
||||
return validateExternalUrl(href, source)
|
||||
.return(href)
|
||||
}
|
||||
|
||||
return findUrlByFile(sidebar, href)
|
||||
return validateLocalFile(sidebar, href, source, render)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -111,7 +176,9 @@ module.exports = {
|
||||
|
||||
findFileBySource,
|
||||
|
||||
findUrlByFile,
|
||||
getLocalFile,
|
||||
|
||||
validateLocalFile,
|
||||
|
||||
validateAndGetUrl,
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.0.2",
|
||||
"cheerio": "^1.0.0-rc.1",
|
||||
"colors": "^1.1.2",
|
||||
"fs-extra": "^3.0.1",
|
||||
"glob": "^7.1.1",
|
||||
|
||||
@@ -91,7 +91,13 @@ hexo.extend.tag.register('url', function (args) {
|
||||
attrs.target = '_blank'
|
||||
}
|
||||
|
||||
return urlGenerator.validateAndGetUrl(sidebar, attrs.href)
|
||||
// onRender callback to generate
|
||||
// the markdown for each internal document
|
||||
const onRender = (text) => {
|
||||
return hexo.render.render({ text, engine: 'markdown' })
|
||||
}
|
||||
|
||||
return urlGenerator.validateAndGetUrl(sidebar, attrs.href, this.full_source, onRender)
|
||||
.then((href) => {
|
||||
attrs.href = href
|
||||
|
||||
|
||||
@@ -77,21 +77,22 @@ describe "lib/url_generator", ->
|
||||
"guides/cypress-basics/overview.html"
|
||||
)
|
||||
|
||||
context ".findUrlByFile", ->
|
||||
context ".getLocalFile", ->
|
||||
beforeEach ->
|
||||
@sandbox.stub(fs, "stat").returns(Promise.resolve())
|
||||
|
||||
it "stats file", ->
|
||||
urlGenerator.findUrlByFile(data, "as")
|
||||
.then (pathToFile) ->
|
||||
expect(fs.stat).to.be.calledWith(path.resolve("source/api/commands/as.md"))
|
||||
expect(pathToFile).to.eq("/api/commands/as.html")
|
||||
it "requests file", ->
|
||||
urlGenerator.getLocalFile(data, "as")
|
||||
.spread (pathToFile, str) ->
|
||||
expect(pathToFile).to.eq("api/commands/as.html")
|
||||
expect(str).to.be.a("string")
|
||||
|
||||
it "throws when cannot find file", ->
|
||||
fn = ->
|
||||
urlGenerator.findUrlByFile(data, "foo")
|
||||
|
||||
expect(fn).to.throw("Could not find a valid doc file in the sidebar.yml for: foo")
|
||||
urlGenerator.getLocalFile(data, "foo")
|
||||
.then ->
|
||||
throw new Error("should have caught error")
|
||||
.catch (err) ->
|
||||
expect(err.message).to.include("Could not find a valid doc file in the sidebar.yml for: foo")
|
||||
|
||||
context ".validateAndGetUrl", ->
|
||||
it "verifies external url", ->
|
||||
@@ -114,7 +115,55 @@ describe "lib/url_generator", ->
|
||||
.catch (err) ->
|
||||
expect(err.message).to.include("Request to: https://www.google.com failed. (Status Code 500)")
|
||||
|
||||
it "returns absolute path to file", ->
|
||||
urlGenerator.validateAndGetUrl(data, "and#notes")
|
||||
it "verifies local file", ->
|
||||
markdown = "## Notes\nfoobarbaz"
|
||||
|
||||
render = (str) ->
|
||||
expect(str).to.eq(markdown)
|
||||
|
||||
return Promise.resolve("<html><div id='notes'>notes</div></html>")
|
||||
|
||||
@sandbox.stub(fs, "readFile").returns(Promise.resolve(markdown))
|
||||
|
||||
urlGenerator.validateAndGetUrl(data, "and#notes", "", render)
|
||||
.then (pathToFile) ->
|
||||
expect(pathToFile).to.eq("/api/commands/and.html#notes")
|
||||
|
||||
it "fails when hash is not present in response", ->
|
||||
nock("https://www.google.com")
|
||||
.get("/")
|
||||
.reply(200, "<html></html>")
|
||||
|
||||
urlGenerator.validateAndGetUrl(data, "https://www.google.com/#foo", "bar.md")
|
||||
.then ->
|
||||
throw new Error("should have caught error")
|
||||
.catch (err) ->
|
||||
[
|
||||
"Constructing {% url %} tag helper failed"
|
||||
"The source file was: bar.md"
|
||||
"You referenced a hash that does not exist at: https://www.google.com/",
|
||||
"Expected to find an element matching the id: #foo"
|
||||
"The HTML response body was:"
|
||||
"<html></html>"
|
||||
].forEach (msg) ->
|
||||
expect(err.message).to.include(msg)
|
||||
|
||||
it "fails when hash is not present in local file", ->
|
||||
render = (str) ->
|
||||
return Promise.resolve("<html></html>")
|
||||
|
||||
@sandbox.stub(fs, "readFile").returns(Promise.resolve(""))
|
||||
|
||||
urlGenerator.validateAndGetUrl(data, "and#foo", "guides/core-concepts/bar.md", render)
|
||||
.then ->
|
||||
throw new Error("should have caught error")
|
||||
.catch (err) ->
|
||||
[
|
||||
"Constructing {% url %} tag helper failed"
|
||||
"The source file was: guides/core-concepts/bar.md"
|
||||
"You referenced a hash that does not exist at: api/commands/and.html",
|
||||
"Expected to find an element matching the id: #foo"
|
||||
"The HTML response body was:"
|
||||
"<html></html>"
|
||||
].forEach (msg) ->
|
||||
expect(err.message).to.include(msg)
|
||||
|
||||
Reference in New Issue
Block a user