diff --git a/docs/index.html b/docs/index.html index f15c4b945e90..01958e47fb73 100644 --- a/docs/index.html +++ b/docs/index.html @@ -27,7 +27,24 @@ style: 'flat' }, logo: 'assets/logo_icon.svg height="64px"', - search: 'auto', + search: { + maxAge: 3600000, // Expiration time - 1 hour + paths: 'auto', + sidebars: [ + '/', + '/istio/user/', + '/btp-manager/user/', + '/application-connector-manager/user/', + '/keda-manager/user/', + '/serverless-manager/user/', + '/telemetry-manager/user/', + '/nats-manager/user/', + '/eventing-manager/user/', + '/api-gateway/user/', + '/cloud-manager/user/', + '/docker-registry/user/' + ] + }, name: 'Kyma Project', repo: '', loadSidebar: true, @@ -50,38 +67,38 @@ '/api-gateway/(.*)': 'https://raw.githubusercontent.com/kyma-project/api-gateway/main/docs/$1', '/cloud-manager/(.*)': 'https://raw.githubusercontent.com/kyma-project/cloud-manager/main/docs/$1', '/docker-registry/(.*)': 'https://raw.githubusercontent.com/kyma-project/docker-registry/main/docs/$1', - }, + }, plugins: [ function (hook, vm) { - // edit on GitHub link - hook.beforeEach(function (html) { - if (/githubusercontent\.com/.test(vm.route.file)) { - url = vm.route.file - .replace('raw.githubusercontent.com', 'github.com') - .replace(/\/main/, '/blob/main') - .replace(/\/release-\d+\.\d+/, '/blob$&'); - } else { - url = 'https://github.com/kyma-project/kyma/blob/main/docs/' + vm.route.file - } - var editHtml = '[:memo: Edit on GitHub](' + url + ')\n' + // edit on GitHub link + hook.beforeEach(function (html) { + if (/githubusercontent\.com/.test(vm.route.file)) { + url = vm.route.file + .replace('raw.githubusercontent.com', 'github.com') + .replace(/\/main/, '/blob/main') + .replace(/\/release-\d+\.\d+/, '/blob$&'); + } else { + url = 'https://github.com/kyma-project/kyma/blob/main/docs/' + vm.route.file + } + var editHtml = '[:memo: Edit on GitHub](' + url + ')\n' - return editHtml - + html - }) + return editHtml + + html + }) }, function noAliasesForImages(hook, vm) { - + // remove title metadata section hook.beforeEach(function (markdown) { markdown = markdown.replaceAll(/---.*\ntitle: (.*)\n---.*\n/gm, "# $1"); - markdown = markdown.replace(/(
)((.|\n)*?)(<\/div>)/gm, function(match, g1, g2, g3, g4) { + markdown = markdown.replace(/(
)((.|\n)*?)(<\/div>)/gm, function (match, g1, g2, g3, g4) { let tab = g2.replace(/((.|\n)*?)(\n|\s)*(.*?)(\n|\s)*<\/summary>((.|\n)*?).*?<\/details>/gm, "#### **$4**\n$6\n"); return '\n' + tab + '\n'; }); return markdown }); - + // replace aliases with absolute urls for images hook.afterEach(function (html) { let path = vm.route.path.split('/') @@ -106,7 +123,7 @@ - + diff --git a/docs/search/component.js b/docs/search/component.js new file mode 100644 index 000000000000..344058662b35 --- /dev/null +++ b/docs/search/component.js @@ -0,0 +1,250 @@ +/* eslint-disable no-unused-vars */ +import { search } from './search.js'; + +let NO_DATA_TEXT = ''; +let options; + +function style() { + const code = ` +.sidebar { + padding-top: 0; +} + +.search { + margin-bottom: 20px; + padding: 6px; + border-bottom: 1px solid #eee; +} + +.search .input-wrap { + display: flex; + align-items: center; +} + +.search .results-panel { + display: none; +} + +.search .results-panel.show { + display: block; +} + +.search input { + outline: none; + border: none; + width: 100%; + padding: 0.6em 7px; + font-size: inherit; + border: 1px solid transparent; +} + +.search input:focus { + box-shadow: 0 0 5px var(--theme-color, #42b983); + border: 1px solid var(--theme-color, #42b983); +} + +.search input::-webkit-search-decoration, +.search input::-webkit-search-cancel-button, +.search input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.search input::-ms-clear { + display: none; + height: 0; + width: 0; +} + +.search .clear-button { + cursor: pointer; + width: 36px; + text-align: right; + display: none; +} + +.search .clear-button.show { + display: block; +} + +.search .clear-button svg { + transform: scale(.5); +} + +.search h2 { + font-size: 17px; + margin: 10px 0; +} + +.search a { + text-decoration: none; + color: inherit; +} + +.search .matching-post { + border-bottom: 1px solid #eee; +} + +.search .matching-post:last-child { + border-bottom: 0; +} + +.search p { + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.search p.empty { + text-align: center; +} + +.app-name.hide, .sidebar-nav.hide { + display: none; +}`; + + Docsify.dom.style(code); +} + +function tpl(defaultValue = '') { + const html = `
+ +
+ + + + + +
+
+
+
`; + const el = Docsify.dom.create('div', html); + const aside = Docsify.dom.find('aside'); + + Docsify.dom.toggleClass(el, 'search'); + Docsify.dom.before(aside, el); +} + +function doSearch(value) { + const $search = Docsify.dom.find('div.search'); + const $panel = Docsify.dom.find($search, '.results-panel'); + const $clearBtn = Docsify.dom.find($search, '.clear-button'); + const $sidebarNav = Docsify.dom.find('.sidebar-nav'); + const $appName = Docsify.dom.find('.app-name'); + + if (!value) { + $panel.classList.remove('show'); + $clearBtn.classList.remove('show'); + $panel.innerHTML = ''; + + if (options.hideOtherSidebarContent) { + $sidebarNav && $sidebarNav.classList.remove('hide'); + $appName && $appName.classList.remove('hide'); + } + + return; + } + + const matchs = search(value); + + let html = ''; + matchs.forEach(post => { + html += `
+ +

${post.title}

+

${post.content}

+
+
`; + }); + + $panel.classList.add('show'); + $clearBtn.classList.add('show'); + $panel.innerHTML = html || `

${NO_DATA_TEXT}

`; + if (options.hideOtherSidebarContent) { + $sidebarNav && $sidebarNav.classList.add('hide'); + $appName && $appName.classList.add('hide'); + } +} + +function bindEvents() { + const $search = Docsify.dom.find('div.search'); + const $input = Docsify.dom.find($search, 'input'); + const $inputWrap = Docsify.dom.find($search, '.input-wrap'); + + let timeId; + + /** + Prevent to Fold sidebar. + + When searching on the mobile end, + the sidebar is collapsed when you click the INPUT box, + making it impossible to search. + */ + Docsify.dom.on( + $search, + 'click', + e => + ['A', 'H2', 'P', 'EM'].indexOf(e.target.tagName) === -1 && + e.stopPropagation() + ); + Docsify.dom.on($input, 'input', e => { + clearTimeout(timeId); + timeId = setTimeout(_ => doSearch(e.target.value.trim()), 100); + }); + Docsify.dom.on($inputWrap, 'click', e => { + // Click input outside + if (e.target.tagName !== 'INPUT') { + $input.value = ''; + doSearch(); + } + }); +} + +function updatePlaceholder(text, path) { + const $input = Docsify.dom.getNode('.search input[type="search"]'); + + if (!$input) { + return; + } + + if (typeof text === 'string') { + $input.placeholder = text; + } else { + const match = Object.keys(text).filter(key => path.indexOf(key) > -1)[0]; + $input.placeholder = text[match]; + } +} + +function updateNoData(text, path) { + if (typeof text === 'string') { + NO_DATA_TEXT = text; + } else { + const match = Object.keys(text).filter(key => path.indexOf(key) > -1)[0]; + NO_DATA_TEXT = text[match]; + } +} + +function updateOptions(opts) { + options = opts; +} + +export function init(opts, vm) { + const keywords = vm.router.parse().query.s; + + updateOptions(opts); + style(); + tpl(keywords); + bindEvents(); + keywords && setTimeout(_ => doSearch(keywords), 500); +} + +export function update(opts, vm) { + updateOptions(opts); + updatePlaceholder(opts.placeholder, vm.route.path); + updateNoData(opts.noData, vm.route.path); +} \ No newline at end of file diff --git a/docs/search/index.js b/docs/search/index.js new file mode 100644 index 000000000000..1b40cf251809 --- /dev/null +++ b/docs/search/index.js @@ -0,0 +1,49 @@ +/* eslint-disable no-unused-vars */ +import { init as initComponent, update as updateComponent } from './component.js'; +import { init as initSearch } from './search.js'; + +const CONFIG = { + placeholder: 'Type to search', + noData: 'No Results!', + paths: 'auto', + depth: 2, + maxAge: 86400000, // 1 day + hideOtherSidebarContent: false, + namespace: undefined, + pathNamespaces: undefined, + pathsWithSidebars: undefined +}; + +const install = function (hook, vm) { + const { util } = Docsify; + const opts = vm.config.search || CONFIG; + + if (Array.isArray(opts)) { + CONFIG.paths = opts; + } else if (typeof opts === 'object') { + CONFIG.paths = Array.isArray(opts.paths) ? opts.paths : 'auto'; + CONFIG.maxAge = util.isPrimitive(opts.maxAge) ? opts.maxAge : CONFIG.maxAge; + CONFIG.placeholder = opts.placeholder || CONFIG.placeholder; + CONFIG.noData = opts.noData || CONFIG.noData; + CONFIG.depth = opts.depth || CONFIG.depth; + CONFIG.hideOtherSidebarContent = + opts.hideOtherSidebarContent || CONFIG.hideOtherSidebarContent; + CONFIG.namespace = opts.namespace || CONFIG.namespace; + CONFIG.pathNamespaces = opts.pathNamespaces || CONFIG.pathNamespaces; + CONFIG.sidebars = opts.sidebars + } + + const isAuto = CONFIG.paths === 'auto'; + + hook.mounted(_ => { + initComponent(CONFIG, vm); + !isAuto && initSearch(CONFIG, vm); + }); + hook.doneEach(_ => { + updateComponent(CONFIG, vm); + isAuto && initSearch(CONFIG, vm); + }); +}; + +window.$docsify = window.$docsify || {}; +$docsify.plugins = [].concat(install, $docsify.plugins); \ No newline at end of file diff --git a/docs/search/search.js b/docs/search/search.js new file mode 100644 index 000000000000..1f87329b95fa --- /dev/null +++ b/docs/search/search.js @@ -0,0 +1,341 @@ + +let INDEXS = {}; + +const LOCAL_STORAGE = { + EXPIRE_KEY: 'docsify.search.expires', + INDEX_KEY: 'docsify.search.index', +}; + +function resolveExpireKey(namespace) { + return namespace + ? `${LOCAL_STORAGE.EXPIRE_KEY}/${namespace}` + : LOCAL_STORAGE.EXPIRE_KEY; +} + +function resolveIndexKey(namespace) { + return namespace + ? `${LOCAL_STORAGE.INDEX_KEY}/${namespace}` + : LOCAL_STORAGE.INDEX_KEY; +} + +function escapeHtml(string) { + const entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + + return String(string).replace(/[&<>"']/g, s => entityMap[s]); +} + +function getAllPaths(router) { + const paths = []; + + Docsify.dom + .findAll('.sidebar-nav a:not(.section-link):not([data-nosearch])') + .forEach(node => { + const href = node.href; + const originHref = node.getAttribute('href'); + const path = router.parse(href).path; + + if ( + path && + paths.indexOf(path) === -1 && + !Docsify.util.isAbsolutePath(originHref) + ) { + paths.push(path); + } + }); + + return paths; +} + +function getTableData(token) { + if (!token.text && token.type === 'table') { + token.cells.unshift(token.header); + token.text = token.cells + .map(function (rows) { + return rows.join(' | '); + }) + .join(' |\n '); + } + return token.text; +} + +function getListData(token) { + if (!token.text && token.type === 'list') { + token.text = token.raw; + } + return token.text; +} + +function saveData(maxAge, expireKey, indexKey) { + localStorage.setItem(expireKey, Date.now() + maxAge); + localStorage.setItem(indexKey, JSON.stringify(INDEXS)); +} + +export function genIndex(path, content = '', router, depth) { + const tokens = window.marked.lexer(content); + const slugify = window.Docsify.slugify; + const index = {}; + let slug; + let title = ''; + + tokens.forEach(function (token, tokenIndex) { + if (token.type === 'heading' && token.depth <= depth) { + slug = router.toURL(path, { id: slugify(escapeHtml(token.text)) }); + title = token.text; + + index[slug] = { slug, title: title, body: '' }; + } else { + if (tokenIndex === 0) { + slug = router.toURL(path); + index[slug] = { + slug, + title: path !== '/' ? path.slice(1) : 'Home Page', + body: token.text || '', + }; + } + + if (!slug) { + return; + } + + if (!index[slug]) { + index[slug] = { slug, title: '', body: '' }; + } else if (index[slug].body) { + token.text = getTableData(token); + token.text = getListData(token); + + index[slug].body += '\n' + (token.text || ''); + } else { + token.text = getTableData(token); + token.text = getListData(token); + + index[slug].body = token.text || ''; + } + } + }); + slugify.clear(); + return index; +} + +export function ignoreDiacriticalMarks(keyword) { + if (keyword && keyword.normalize) { + return keyword.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + } + return keyword; +} + +/** + * @param {String} query Search query + * @returns {Array} Array of results + */ +export function search(query) { + const matchingResults = []; + let data = []; + Object.keys(INDEXS).forEach(key => { + data = data.concat(Object.keys(INDEXS[key]).map(page => INDEXS[key][page])); + }); + + query = query.trim(); + let keywords = query.split(/[\s\-,\\/]+/); + if (keywords.length !== 1) { + keywords = [].concat(query, keywords); + } + + for (let i = 0; i < data.length; i++) { + const post = data[i]; + let matchesScore = 0; + let resultStr = ''; + let handlePostTitle = ''; + let handlePostContent = ''; + const postTitle = post.title && post.title.trim(); + const postContent = post.body && post.body.trim(); + const postUrl = post.slug || ''; + + if (postTitle) { + keywords.forEach(keyword => { + // From https://github.com/sindresorhus/escape-string-regexp + const regEx = new RegExp( + escapeHtml(ignoreDiacriticalMarks(keyword)).replace( + /[|\\{}()[\]^$+*?.]/g, + '\\$&' + ), + 'gi' + ); + let indexTitle = -1; + let indexContent = -1; + handlePostTitle = postTitle + ? escapeHtml(ignoreDiacriticalMarks(postTitle)) + : postTitle; + handlePostContent = postContent + ? escapeHtml(ignoreDiacriticalMarks(postContent)) + : postContent; + + indexTitle = postTitle ? handlePostTitle.search(regEx) : -1; + indexContent = postContent ? handlePostContent.search(regEx) : -1; + + if (indexTitle >= 0 || indexContent >= 0) { + matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0; + if (indexContent < 0) { + indexContent = 0; + } + + let start = 0; + let end = 0; + + start = indexContent < 11 ? 0 : indexContent - 10; + end = start === 0 ? 70 : indexContent + keyword.length + 60; + + if (postContent && end > postContent.length) { + end = postContent.length; + } + + const matchContent = + handlePostContent && + '...' + + handlePostContent + .substring(start, end) + .replace( + regEx, + word => `${word}` + ) + + '...'; + + resultStr += matchContent; + } + }); + + if (matchesScore > 0) { + const matchingPost = { + title: handlePostTitle, + content: postContent ? resultStr : '', + url: postUrl, + score: matchesScore, + }; + + matchingResults.push(matchingPost); + } + } + } + + return matchingResults.sort((r1, r2) => r2.score - r1.score); +} + +function extractLinksFromMarkdown(markdownText) { + const linkRegex = /\[([^\]]+)\]\(([^)\s]+)\)/g; + const links = []; + let match; + + while ((match = linkRegex.exec(markdownText)) !== null) { + links.push({ + text: match[1], // The text part of the markdown link + url: match[2] // The URL part of the markdown link (can be relative or absolute) + }); + } + + return links; +} + +async function pathsFromSidebars(sidebarsPaths, router) { + const paths = [] + const tasks = [] + for (const path of sidebarsPaths) { + tasks.push( + Docsify.get(router.getFile(path + "_sidebar.md"), false, router.config.requestHeaders)) + } + const res = await Promise.allSettled(tasks) + for (const [i, r] of res.entries()) { + if (r.status === 'fulfilled') { + const path = sidebarsPaths[i] + const links = extractLinksFromMarkdown(r.value) + for (const l of links) { + l.url = l.url.startsWith('/') ? l.url : path + l.url + //trim the '.md' extension + l.url = l.url.replace(/\.md$/, '') + if (paths.indexOf(l.url) === -1) { + paths.push(l.url) + } + } + } + } + + return paths +} + +export async function init(config, vm) { + const isAuto = config.paths === 'auto'; + let paths = [] + if (config.sidebars) { + paths = await pathsFromSidebars(config.sidebars, vm.router) + } else { + paths = isAuto ? getAllPaths(vm.router) : config.paths; + } + + let namespaceSuffix = ''; + + // only in auto mode + if (paths.length && isAuto && config.pathNamespaces) { + const path = paths[0]; + + if (Array.isArray(config.pathNamespaces)) { + namespaceSuffix = + config.pathNamespaces.filter( + prefix => path.slice(0, prefix.length) === prefix + )[0] || namespaceSuffix; + } else if (config.pathNamespaces instanceof RegExp) { + const matches = path.match(config.pathNamespaces); + + if (matches) { + namespaceSuffix = matches[0]; + } + } + const isExistHome = paths.indexOf(namespaceSuffix + '/') === -1; + const isExistReadme = paths.indexOf(namespaceSuffix + '/README') === -1; + if (isExistHome && isExistReadme) { + paths.unshift(namespaceSuffix + '/'); + } + } else if (paths.indexOf('/') === -1 && paths.indexOf('/README') === -1) { + paths.unshift('/'); + } + const expireKey = resolveExpireKey(config.namespace) + namespaceSuffix; + const indexKey = resolveIndexKey(config.namespace) + namespaceSuffix; + + const isExpired = localStorage.getItem(expireKey) < Date.now(); + + INDEXS = JSON.parse(localStorage.getItem(indexKey)); + + if (isExpired) { + INDEXS = {}; + } else if (!isAuto) { + return; + } + + // if any path is not indexed, invalidate the cache + if (paths.some(path => !INDEXS[path])) { + console.log("Some paths not indexed") + INDEXS = {}; + } else { + return; // all paths are indexed + } + + const len = paths.length; + let count = 0; + + paths.forEach(path => { + + if (INDEXS[path]) { + return count++; + } + + Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then( + result => { + INDEXS[path] = genIndex(path, result, vm.router, config.depth); + len === ++count && saveData(config.maxAge, expireKey, indexKey); + } + ); + }); +} \ No newline at end of file