1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-20 21:59:41 +02:00

search implementation

This commit is contained in:
Taly 2022-08-11 18:28:15 +03:00
parent f05eb15b72
commit 79592f0a1d
12 changed files with 440 additions and 28 deletions

View file

@ -9,5 +9,5 @@
"watch": [ "watch": [
"**/*" "**/*"
], ],
"ext": "js,twig" "ext": "ts,js,twig"
} }

View file

@ -45,6 +45,7 @@
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
"@codexteam/misprints": "^1.0.0", "@codexteam/misprints": "^1.0.0",
"@codexteam/shortcuts": "^1.2.0",
"@editorjs/checklist": "^1.3.0", "@editorjs/checklist": "^1.3.0",
"@editorjs/code": "^2.7.0", "@editorjs/code": "^2.7.0",
"@editorjs/delimiter": "^1.2.0", "@editorjs/delimiter": "^1.2.0",
@ -81,6 +82,7 @@
"@types/sinon": "^10.0.2", "@types/sinon": "^10.0.2",
"@types/twig": "^1.12.6", "@types/twig": "^1.12.6",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^0.27.2",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",

View file

@ -0,0 +1,128 @@
import Page from '../models/page';
import Pages from '../controllers/pages';
type SearchResponse = {
completions: string[];
pages: Page[];
}
class Search {
/**
* Prepare words database
*/
public async index() {
/**
* Prepare pages content for the search
* @todo - it should be done in the background
*/
const pages = await Pages.getAll();
const pagesWords = pages.map(page => {
const pageWords: string[] = [];
page.body.blocks.forEach((block: any) => {
let blockContent = '';
const validBlocks = ['header', 'paragraph'];
if (!validBlocks.includes(block.type)) {
return;
}
switch (block.type) {
case 'header':
blockContent = block.data.text;
break;
case 'paragraph':
blockContent = block.data.text
break;
}
const blockWords: string[] = blockContent
// @todo get text from inline code elements and remove html tags
// left only letters and numbers
.replace(/[^a-z0-9]/gi, ' ')
// lowercase all words
.toLowerCase()
// remove multiple spaces
.replace(/\s+/g, ' ')
// split to words by spaces
.split(' ');
pageWords.push(...blockWords);
});
const uniqueWords = [...new Set(pageWords)].sort();
return {
id: page._id,
words: uniqueWords
};
});
return pagesWords;
}
public async query(searchString: string): Promise<SearchResponse> {
const pages = await Pages.getAll();
const pagesWords = await this.index();
/**
* Search itself
*/
const searchWords = searchString.toLowerCase().split(' ');
const goodPages = pagesWords.map(({ id, words}) => {
const foundWords = searchWords.filter(
word => {
return words.filter(
testWord => {
return testWord.indexOf(word) === 0
}
).length > 0;
}
);
const successRatio = foundWords.length / searchWords.length * 100;
return {
id,
successRatio
}
});
const foundPages = goodPages
.filter(({ successRatio }) => successRatio > 50)
.sort((a, b) => b.successRatio - a.successRatio)
.slice(0, 10);
// --------- START test ---------
const uniqWords = [...new Set(pagesWords.flatMap(page => page.words))].sort();
uniqWords.forEach(word => {
console.log(word);
})
// --------- END test ---------
return {
completions: uniqWords.filter(word => word.indexOf(searchWords.slice(-1)[0]) === 0),
pages: pages.filter(page => foundPages.some(({ id }) => id === page._id))
}
}
private async search(searchString: string) {
const pages = await this.query(searchString);
return pages;
}
}
export default Search;

View file

@ -1,12 +1,15 @@
import express from 'express'; import express from 'express';
import pagesAPI from './pages'; import pagesAPI from './pages';
import transportAPI from './transport'; import transportAPI from './transport';
import linksAPI from './links'; import linksAPI from './links';
import searchAPI from './search';
const router = express.Router(); const router = express.Router();
router.use('/', pagesAPI); router.use('/', pagesAPI);
router.use('/', transportAPI); router.use('/', transportAPI);
router.use('/', linksAPI); router.use('/', linksAPI);
router.use('/', searchAPI);
export default router; export default router;

View file

@ -0,0 +1,54 @@
import express, { Request, Response } from 'express';
import Search from '../../controllers/search';
const router = express.Router();
/**
* GET /search/:searchString
*
* Search given words in all documents
*/
router.get('/search', async (req: Request, res: Response) => {
try {
const searchString = req.query.text as string;
/** Start measuring search time */
const startTime = performance.now();
const search = new Search();
const searchResponse = await search.query(searchString);
/** End measuring search time */
const endTime = performance.now();
/** Show search time */
const searchItem = (endTime - startTime).toFixed(6);
console.log(`🔎 "${searchString}" ⏱ ${searchItem} ms`);
const compactedPages = searchResponse.pages.map(page => {
return {
_id: page._id,
title: page.title,
uri: page.uri,
// body: page.body,
// parent: page.parent,
};
});
res.json({
success: true,
result: {
completions: searchResponse.completions,
pages: compactedPages,
time: searchItem,
},
});
} catch (err) {
res.status(400).json({
success: false,
error: (err as Error).message,
});
}
});
export default router;

View file

@ -15,7 +15,7 @@
hawkClientToken:"{{ config.hawkClientToken }}", hawkClientToken:"{{ config.hawkClientToken }}",
}; };
</script> </script>
<body> <body data-module="search">
{% include "components/header.twig" with res.locals.isAuthorized %} {% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs"> <div class="docs">

View file

@ -17,6 +17,7 @@ import Writing from './modules/writing';
import Page from './modules/page'; import Page from './modules/page';
import Extensions from './modules/extensions'; import Extensions from './modules/extensions';
import Sidebar from './modules/sidebar'; import Sidebar from './modules/sidebar';
import Search from './modules/search';
import HawkCatcher from '@hawk.so/javascript'; import HawkCatcher from '@hawk.so/javascript';
/** /**
@ -31,6 +32,8 @@ class Docs {
this.page = new Page(); this.page = new Page();
this.extensions = new Extensions(); this.extensions = new Extensions();
this.sidebar = new Sidebar(); this.sidebar = new Sidebar();
this.search = new Search();
if (window.config.hawkClientToken) { if (window.config.hawkClientToken) {
this.hawk = new HawkCatcher(window.config.hawkClientToken); this.hawk = new HawkCatcher(window.config.hawkClientToken);
} }

View file

@ -17,6 +17,7 @@ export default class Page {
init() { init() {
this.codeStyler = this.createCodeStyling(); this.codeStyler = this.createCodeStyling();
this.tableOfContent = this.createTableOfContent(); this.tableOfContent = this.createTableOfContent();
this.search = this.createSearch();
} }
/** /**

View file

@ -0,0 +1,137 @@
import { debounce } from '../utils/decorators';
import Shortcut from '@codexteam/shortcuts';
import axios from 'axios';
export default class Search {
constructor() {
this.nodes = {
overlay: null,
searchWrapper: null,
searchInput: null,
searchResultsWrapper: null
};
this.isVisible = false;
this.shortcut = null;
this.TOGGLER_SHORTCUT = 'CMD+SHIFT+F';
this.debouncedSearch = null;
this.DEBOUNCE_TIME = 500;
}
init(settings = {}, moduleEl) {
console.log('search init');
this.createSearchOverlay();
this.createDebouncedSearch();
this.enableShortcutListening();
// ! force open search overlay
// this.toggleSearchOverlay(true);
}
createSearchOverlay() {
this.nodes.overlay = document.createElement('div');
this.nodes.overlay.classList.add('search-overlay');
this.nodes.overlay.addEventListener('click', this.searchOverlayClickProcessor.bind(this));
this.nodes.searchWrapper = document.createElement('div');
this.nodes.searchWrapper.classList.add('search-wrapper');
this.nodes.searchInput = document.createElement('input');
this.nodes.searchInput.classList.add('search-input');
this.nodes.searchInput.setAttribute('type', 'search');
this.nodes.searchInput.setAttribute('placeholder', 'Find in documents...');
this.nodes.searchInput.setAttribute('autocomplete', 'off');
this.nodes.searchInput.addEventListener('input', this.searchInputOnchangeProcessor.bind(this));
this.nodes.searchWrapper.appendChild(this.nodes.searchInput);
this.nodes.searchResultsWrapper = document.createElement('div');
this.nodes.searchResultsWrapper.classList.add('search-results-wrapper');
this.nodes.searchWrapper.appendChild(this.nodes.searchResultsWrapper);
this.nodes.overlay.appendChild(this.nodes.searchWrapper);
document.body.appendChild(this.nodes.overlay);
}
searchOverlayClickProcessor(event) {
if (event.target !== this.nodes.overlay) {
return;
}
this.toggleSearchOverlay(false);
}
searchInputOnchangeProcessor(event) {
// close search overlay if ESC key is pressed
if (event.keyCode === 27) {
this.toggleSearchOverlay(false);
event.preventDefault();
}
console.log(event.target.value);
this.debouncedSearch(event.target.value);
}
enableShortcutListening() {
this.shortcut = new Shortcut({
name : this.TOGGLER_SHORTCUT,
on : document.body,
callback: (event) => {
this.toggleSearchOverlay();
}
});
}
toggleSearchOverlay(force) {
this.isVisible = force || !this.isVisible;
this.nodes.overlay.classList.toggle('search-overlay--visible', this.isVisible);
document.body.classList.toggle('noscroll', this.isVisible);
this.nodes.searchInput.focus();
}
createDebouncedSearch() {
this.debouncedSearch = debounce(this.getSearchResults, this.DEBOUNCE_TIME);
}
getSearchResults(text) {
// this.showSearchResult(text);
// call api to get search results
axios.get('/api/search', {
params: {
text: text
}
})
.then(this.showSearchResult.bind(this));
}
showSearchResult({ data }) {
console.log(data);
this.nodes.searchResultsWrapper.innerHTML = '';
data.result.pages.forEach(page => {
const result = document.createElement('a');
result.classList.add('search-results-item');
result.setAttribute('href', `/${page.uri}`);
const title = document.createElement('div');
title.classList.add('search-results-item__title');
title.innerHTML = page.title;
result.appendChild(title);
// const description = document.createElement('div');
// description.classList.add('search-results-item__description');
// result.appendChild(description);
this.nodes.searchResultsWrapper.appendChild(result);
});
}
}

View file

@ -0,0 +1,78 @@
.search {
&-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
overflow-y: scroll;
cursor: pointer;
&--visible {
display: block;
}
}
&-wrapper {
background: #fff;
margin: 30vh auto 10vh;
max-width: var(--layout-width-main-col);
width: 100%;
@apply --squircle;
}
&-input {
width: 100%;
background: #bbb;
padding: 15px 25px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
outline: none;
background-color: transparent;
&::placeholder {
font-weight: 500;
}
}
&-results {
&-wrapper {
}
&-item {
padding: 15px 25px;
display: block;
&:nth-child(odd) {
background: #f5f5f5;
}
&:hover {
background: #ddd;
}
&__title {
font-weight: 500;
}
&__description {
}
}
}
}
.noscroll {
overflow: hidden;
}

View file

@ -12,6 +12,7 @@
@import './components/sidebar.pcss'; @import './components/sidebar.pcss';
@import './components/navigator.pcss'; @import './components/navigator.pcss';
@import './components/table-of-content.pcss'; @import './components/table-of-content.pcss';
@import './components/search.pcss';
body { body {
font-family: system-ui, Helvetica, Arial, Verdana; font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -805,6 +805,11 @@
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@codexteam/misprints/-/misprints-1.0.0.tgz#e5a7dec7389fe0f176cd51a040d6dc9bdc252086" resolved "https://registry.yarnpkg.com/@codexteam/misprints/-/misprints-1.0.0.tgz#e5a7dec7389fe0f176cd51a040d6dc9bdc252086"
"@codexteam/shortcuts@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@codexteam/shortcuts/-/shortcuts-1.2.0.tgz#b8dd7396962b0bd845a5c8f8f19bc6119b520e19"
integrity sha512-Udb8lkwhXEiPoLm7krtUv2f8jYQTutHxsLecmsMvMbOxMJ49LA/EUUzn8Fo32mxOFrI7qozOovspLhHb+y60nQ==
"@cspotcode/source-map-consumer@0.8.0": "@cspotcode/source-map-consumer@0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
@ -1698,6 +1703,14 @@ axios@^0.21.1:
dependencies: dependencies:
follow-redirects "^1.14.0" follow-redirects "^1.14.0"
axios@^0.27.2:
version "0.27.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
dependencies:
follow-redirects "^1.14.9"
form-data "^4.0.0"
babel-eslint@^10.0.1: babel-eslint@^10.0.1:
version "10.1.0" version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@ -3057,7 +3070,7 @@ flatten@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
follow-redirects@^1.14.0: follow-redirects@^1.14.0, follow-redirects@^1.14.9:
version "1.15.1" version "1.15.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
@ -3089,6 +3102,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
formidable@^1.2.0: formidable@^1.2.0:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
@ -3247,18 +3269,6 @@ growl@1.10.5:
version "1.10.5" version "1.10.5"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
handlebars@^4.1.0:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
has-bigints@^1.0.1: has-bigints@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@ -3305,6 +3315,11 @@ hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
htmlparser2@^6.1.0: htmlparser2@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@ -3920,7 +3935,7 @@ make-dir@^1.3.0:
dependencies: dependencies:
pify "^3.0.0" pify "^3.0.0"
make-dir@^2.0.0: make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
dependencies: dependencies:
@ -4147,7 +4162,7 @@ negotiator@0.6.3:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
neo-async@^2.6.0, neo-async@^2.6.2: neo-async@^2.6.2:
version "2.6.2" version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
@ -5597,7 +5612,7 @@ supports-color@^5.3.0, supports-color@^5.4.0:
dependencies: dependencies:
has-flag "^3.0.0" has-flag "^3.0.0"
supports-color@^6.0.0: supports-color@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
@ -5829,11 +5844,6 @@ typescript@^4.3.5:
version "4.6.3" version "4.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
uglify-js@^3.1.4:
version "3.16.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d"
integrity sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==
uid-safe@2.1.5: uid-safe@2.1.5:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
@ -6044,11 +6054,6 @@ word-wrap@^1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
wrap-ansi@^2.0.0: wrap-ansi@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"