Skip to content

Commit

Permalink
feat: add header group show
Browse files Browse the repository at this point in the history
  • Loading branch information
sanyuan0704 committed Oct 6, 2022
1 parent 771ed1c commit 113340b
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 99 deletions.
5 changes: 0 additions & 5 deletions src/node/plugin.ts
Expand Up @@ -9,7 +9,6 @@ import babelPluginIsland from './babel-plugin-island';
import { ISLAND_JSX_RUNTIME_PATH } from './constants/index';
import pluginUnocss from 'unocss/vite';
import unocssOptions from './unocss.config';
// import pluginInspect from 'vite-plugin-inspect';

export async function createIslandPlugins(
config: SiteConfig,
Expand All @@ -18,10 +17,6 @@ export async function createIslandPlugins(
): Promise<PluginOption[]> {
return [
pluginUnocss(unocssOptions),
// pluginInspect({
// dev: false,
// build: true
// }),
// Md(x) compile
await pluginMdx(config, isServer),
// For island internal use
Expand Down
61 changes: 61 additions & 0 deletions src/theme-default/components/Search/Suggestion.tsx
@@ -0,0 +1,61 @@
import type { MatchResultItem } from '../../logic/search';

export function SuggestionContent(props: {
suggestion: MatchResultItem;
query: string;
isCurrent: boolean;
}) {
const { suggestion, query } = props;
const renderHeaderMatch = () => {
if (suggestion.type === 'header') {
const { header, headerHighlightIndex } = suggestion;
const headerPrefix = header.slice(0, headerHighlightIndex);
const headerSuffix = header.slice(headerHighlightIndex + query.length);
return (
<div font="medium">
<span>{headerPrefix}</span>
<span bg="brand-light" p="y-0.4 x-0.8" rounded="md" text="text-1">
{query}
</span>
<span>{headerSuffix}</span>
</div>
);
} else {
return <div font="medium">{suggestion.header}</div>;
}
};
const renderStatementMatch = () => {
if (suggestion.type !== 'content') {
return;
}
const { statementHighlightIndex, statement } = suggestion;
const statementPrefix = statement.slice(0, statementHighlightIndex);
const statementSuffix = statement.slice(
statementHighlightIndex + query.length
);
return (
<div font="normal" text="sm gray-light" w="100%">
<span>{statementPrefix}</span>
<span bg="brand-light" p="y-0.4 x-0.8" rounded="md" text="[#000]">
{query}
</span>
<span>{statementSuffix}</span>
</div>
);
};
return (
<div
border-1=""
table-cell=""
p="x-3 y-2"
hover="bg-[#f3f4f5]"
className={`border-right-none ${props.isCurrent ? 'bg-[#f3f4f5]' : ''}`}
transition="bg duration-200"
>
<div font="medium" text="sm">
{renderHeaderMatch()}
</div>
{suggestion.type === 'content' && renderStatementMatch()}
</div>
);
}
61 changes: 1 addition & 60 deletions src/theme-default/components/Search/index.tsx
Expand Up @@ -4,73 +4,14 @@ import { ComponentPropsWithIsland } from '../../../shared/types/index';
import SearchSvg from './icons/search.svg';
import LoadingSvg from './icons/loading.svg';
import { throttle } from 'lodash-es';
import { SuggestionContent } from './Suggestion';

const KEY_CODE = {
ARROW_UP: 'ArrowUp',
ARROW_DOWN: 'ArrowDown',
ENTER: 'Enter'
};

function SuggestionContent(props: {
suggestion: MatchResultItem;
query: string;
isCurrent: boolean;
}) {
const { suggestion, query } = props;
const renderHeaderMatch = () => {
if (suggestion.type === 'header') {
const { header, headerHighlightIndex } = suggestion;
const headerPrefix = header.slice(0, headerHighlightIndex);
const headerSuffix = header.slice(headerHighlightIndex + query.length);
return (
<div font="medium">
<span>{headerPrefix}</span>
<span bg="brand-light" p="y-0.4 x-0.8" rounded="md" text="text-1">
{query}
</span>
<span>{headerSuffix}</span>
</div>
);
} else {
return <div font="medium">{suggestion.header}</div>;
}
};
const renderStatementMatch = () => {
if (suggestion.type !== 'content') {
return;
}
const { statementHighlightIndex, statement } = suggestion;
const statementPrefix = statement.slice(0, statementHighlightIndex);
const statementSuffix = statement.slice(
statementHighlightIndex + query.length
);
return (
<div font="normal" text="sm gray-light" w="100%">
<span>{statementPrefix}</span>
<span bg="brand-light" p="y-0.4 x-0.8" rounded="md" text="[#000]">
{query}
</span>
<span>{statementSuffix}</span>
</div>
);
};
return (
<div
border-1=""
table-cell=""
p="x-3 y-2"
hover="bg-[#f3f4f5]"
className={`border-right-none ${props.isCurrent ? 'bg-[#f3f4f5]' : ''}`}
transition="bg duration-200"
>
<div font="medium" text="sm">
{renderHeaderMatch()}
</div>
{suggestion.type === 'content' && renderStatementMatch()}
</div>
);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Search(
props: ComponentPropsWithIsland & { langRoutePrefix: string }
Expand Down
29 changes: 1 addition & 28 deletions src/theme-default/logic/index.ts
@@ -1,35 +1,8 @@
export const isProduction = () => import.meta.env.PROD;

export function addLeadingSlash(url: string) {
return url.charAt(0) === '/' ? url : '/' + url;
}

export function removeTrailingSlash(url: string) {
return url.charAt(url.length - 1) === '/' ? url.slice(0, -1) : url;
}

export function normalizeHref(url?: string) {
if (!url) {
return '/';
}
if (!isProduction() || url.startsWith('http')) {
return url;
}

let suffix = '';
if (!import.meta.env.ENABLE_SPA) {
suffix += '.html';
if (url.endsWith('/')) {
suffix = 'index' + suffix;
}
}
return addLeadingSlash(`${url}${suffix}`);
}

export { usePrevNextPage } from './usePrevNextPage';
export { useEditLink } from './useEditLink';
export { useSidebarData } from './useSidebarData';
export { useLocaleSiteData } from './useLocaleSiteData';
export { setupEffects, bindingAsideScroll } from './sideEffects';
export { setupCopyCodeButton } from './copyCode';
export { PageSearcher } from './search';
export * from './utils';
21 changes: 15 additions & 6 deletions src/theme-default/logic/search.ts
Expand Up @@ -3,6 +3,8 @@ import type { Index as SearchIndex, CreateOptions } from 'flexsearch';
import { getAllPages } from 'island/client';
import { uniqBy } from 'lodash-es';
import { normalizeHref } from './index';
import { Header } from 'shared/types/index';
import { backTrackHeaders } from './utils';

const THRESHOLD_CONTENT_LENGTH = 100;

Expand All @@ -11,6 +13,7 @@ interface PageDataForSearch {
headers: string[];
content: string;
path: string;
rawHeaders: Header[];
}

interface CommonMatchResult {
Expand Down Expand Up @@ -43,7 +46,7 @@ export class PageSearcher {
#headerToIdMap: Record<string, string> = {};
#langRoutePrefix: string;

constructor(langRoutePrefix: string) {
constructor(langRoutePrefix = '') {
this.#langRoutePrefix = langRoutePrefix;
}

Expand All @@ -60,7 +63,8 @@ export class PageSearcher {
title: page.title!,
headers: (page.toc || []).map((header) => header.text),
content: page.content || '',
path: page.routePath
path: page.routePath,
rawHeaders: page.toc || []
}));
this.#headerToIdMap = pages.reduce((acc, page) => {
(page.toc || []).forEach((header) => {
Expand Down Expand Up @@ -135,15 +139,20 @@ export class PageSearcher {
query: string,
matchedResult: MatchResultItem[]
): boolean {
const { headers } = item;
for (const header of headers) {
const { headers, rawHeaders } = item;
for (const [index, header] of headers.entries()) {
if (header.includes(query)) {
const headerAnchor = this.#headerToIdMap[item.path + header];
// Find the all parent headers (except h1)
// So we can show the full path of the header in search result
// e.g. header2 > header3 > header4
const headerGroup = backTrackHeaders(rawHeaders, index);
const headerStr = headerGroup.map((item) => item.text).join(' > ');
matchedResult.push({
type: 'header',
title: item.title,
header,
headerHighlightIndex: header.indexOf(query),
header: headerStr,
headerHighlightIndex: headerStr.indexOf(query),
link: `${normalizeHref(item.path)}#${headerAnchor}`
});
return true;
Expand Down
21 changes: 21 additions & 0 deletions src/theme-default/logic/utils.test.ts
@@ -0,0 +1,21 @@
import { expect, test, describe } from 'vitest';
import { Header } from 'shared/types';
import { backTrackHeaders } from './utils';

describe('utils logic', () => {
test('back track the headers', () => {
const headers: Header[] = [
{ depth: 1, text: '1', id: '1' },
{ depth: 2, text: '2', id: '2' },
{ depth: 3, text: '3', id: '3' },
{ depth: 4, text: '4', id: '4' },
{ depth: 5, text: '5', id: '5' }
];
const res = backTrackHeaders(headers, 3);
expect(res).toEqual([
{ depth: 2, text: '2', id: '2' },
{ depth: 3, text: '3', id: '3' },
{ depth: 4, text: '4', id: '4' }
]);
});
});
51 changes: 51 additions & 0 deletions src/theme-default/logic/utils.ts
@@ -0,0 +1,51 @@
import { Header } from 'shared/types';

export const isProduction = () => import.meta.env.PROD;

export function addLeadingSlash(url: string) {
return url.charAt(0) === '/' ? url : '/' + url;
}

export function removeTrailingSlash(url: string) {
return url.charAt(url.length - 1) === '/' ? url.slice(0, -1) : url;
}

export function normalizeHref(url?: string) {
if (!url) {
return '/';
}
if (!isProduction() || url.startsWith('http')) {
return url;
}

let suffix = '';
if (!import.meta.env.ENABLE_SPA) {
suffix += '.html';
if (url.endsWith('/')) {
suffix = 'index' + suffix;
}
}
return addLeadingSlash(`${url}${suffix}`);
}

export function backTrackHeaders(
rawHeaders: Header[],
index: number
): Header[] {
let current = rawHeaders[index];
let currentIndex = index;

const res: Header[] = [current];
while (current && current.depth > 2) {
for (let i = currentIndex - 1; i >= 0; i--) {
const header = rawHeaders[i];
if (header.depth > 1 && header.depth === current.depth - 1) {
current = header;
currentIndex = i;
res.unshift(current);
break;
}
}
}
return res;
}

0 comments on commit 113340b

Please sign in to comment.