mirror of https://github.com/usememos/memos.git
feat: highlight the searched text
This commit is contained in:
parent
b05a176490
commit
b6b1b5249f
|
|
@ -18,6 +18,7 @@ dayjs.extend(relativeTime);
|
|||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
highlightWord: string | undefined;
|
||||
}
|
||||
|
||||
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
||||
|
|
@ -29,7 +30,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
|||
};
|
||||
|
||||
const Memo: React.FC<Props> = (props: Props) => {
|
||||
const memo = props.memo;
|
||||
const { memo, highlightWord } = props;
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
|
||||
|
|
@ -232,6 +233,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||
</div>
|
||||
<MemoContent
|
||||
content={memo.content}
|
||||
highlightWord={highlightWord}
|
||||
onMemoContentClick={handleMemoContentClick}
|
||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface DisplayConfig {
|
|||
|
||||
interface Props {
|
||||
content: string;
|
||||
highlightWord: string | undefined;
|
||||
className?: string;
|
||||
displayConfig?: Partial<DisplayConfig>;
|
||||
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||
|
|
@ -29,7 +30,7 @@ const defaultDisplayConfig: DisplayConfig = {
|
|||
};
|
||||
|
||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
|
||||
const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props;
|
||||
const foldedContent = useMemo(() => {
|
||||
const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
|
||||
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
|
||||
|
|
@ -86,7 +87,7 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||
onClick={handleMemoContentClick}
|
||||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
dangerouslySetInnerHTML={{ __html: marked(state.expandButtonStatus === 0 ? foldedContent : content) }}
|
||||
dangerouslySetInnerHTML={{ __html: marked(state.expandButtonStatus === 0 ? foldedContent : content, highlightWord) }}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const MemoList = () => {
|
|||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
|
||||
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
|
||||
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
|
||||
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
|
||||
|
||||
const shownMemos =
|
||||
showMemoFilter || shortcut
|
||||
|
|
@ -103,6 +104,7 @@ const MemoList = () => {
|
|||
if (pageWrapper) {
|
||||
pageWrapper.scrollTo(0, 0);
|
||||
}
|
||||
setHighlightWord(textQuery?.toLowerCase());
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -131,7 +133,7 @@ const MemoList = () => {
|
|||
return (
|
||||
<div className="memo-list-container">
|
||||
{sortedMemos.map((memo) => (
|
||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
|
||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
|
||||
))}
|
||||
{isFetching ? (
|
||||
<div className="status-text-container fetching-tip">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ const match = (rawStr: string, regex: RegExp): number => {
|
|||
return matchStr.length;
|
||||
};
|
||||
|
||||
export const marked = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
|
||||
export const marked = (
|
||||
markdownStr: string,
|
||||
highlightWord: string | undefined,
|
||||
blockParsers = blockElementParserList,
|
||||
inlineParsers = inlineElementParserList
|
||||
): string => {
|
||||
for (const parser of blockParsers) {
|
||||
const startIndex = markdownStr.search(parser.regex);
|
||||
const matchedLength = match(markdownStr, parser.regex);
|
||||
|
|
@ -19,7 +24,11 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
|
|||
const prefixStr = markdownStr.slice(0, startIndex);
|
||||
const matchedStr = markdownStr.slice(startIndex, startIndex + matchedLength);
|
||||
const suffixStr = markdownStr.slice(startIndex + matchedLength);
|
||||
return marked(prefixStr, blockParsers, inlineParsers) + parser.renderer(matchedStr) + marked(suffixStr, blockParsers, inlineParsers);
|
||||
return (
|
||||
marked(prefixStr, highlightWord, blockParsers, inlineParsers) +
|
||||
parser.renderer(matchedStr, highlightWord) +
|
||||
marked(suffixStr, highlightWord, blockParsers, inlineParsers)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +56,7 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
|
|||
const prefixStr = markdownStr.slice(0, matchedIndex);
|
||||
const matchedStr = markdownStr.slice(matchedIndex, matchedIndex + matchedLength);
|
||||
const suffixStr = markdownStr.slice(matchedIndex + matchedLength);
|
||||
return prefixStr + matchedInlineParser.renderer(matchedStr) + marked(suffixStr, [], inlineParsers);
|
||||
return prefixStr + matchedInlineParser.renderer(matchedStr, highlightWord) + marked(suffixStr, highlightWord, [], inlineParsers);
|
||||
}
|
||||
|
||||
return markdownStr;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { escape } from "lodash";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const BLOCKQUOTE_REG = /^>\s+(.+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(BLOCKQUOTE_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
return `<blockquote>${escape(matchResult[1])}</blockquote>${matchResult[2]}`;
|
||||
const result = renderWithHighlightWord(escape(matchResult[1]), highlightWord);
|
||||
|
||||
return `<blockquote>${result}</blockquote>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { marked } from "..";
|
||||
import Link from "./Link";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const BOLD_REG = /\*\*(.+?)\*\*/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(BOLD_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], [Link]);
|
||||
return `<strong>${parsedContent}</strong>`;
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], [Link]);
|
||||
return `<strong>${renderWithHighlightWord(parsedContent, highlightWord)}</strong>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { marked } from "..";
|
||||
import Link from "./Link";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const BOLD_EMPHASIS_REG = /\*\*\*(.+?)\*\*\*/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(BOLD_EMPHASIS_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], [Link]);
|
||||
return `<strong><em>${parsedContent}</em></strong>`;
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], [Link]);
|
||||
return `<strong><em>${renderWithHighlightWord(parsedContent, highlightWord)}</em></strong>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { escape } from "lodash-es";
|
||||
import hljs from "highlight.js";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(CODE_BLOCK_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
|
|
@ -21,6 +22,8 @@ const renderer = (rawStr: string): string => {
|
|||
// do nth
|
||||
}
|
||||
|
||||
highlightedCode = renderWithHighlightWord(highlightedCode, highlightWord);
|
||||
|
||||
return `<pre><code class="language-${language}">${highlightedCode}</code></pre>${matchResult[3]}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { marked } from "..";
|
|||
|
||||
export const DONE_LIST_REG = /^- \[x\] (.+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(DONE_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], inlineElementParserList);
|
||||
return `<p><span class='todo-block done' data-value='DONE'>✓</span>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import { marked } from "..";
|
||||
import Link from "./Link";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const EMPHASIS_REG = /\*(.+?)\*/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(EMPHASIS_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], [Link]);
|
||||
return `<em>${parsedContent}</em>`;
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], [Link]);
|
||||
return `<em>${renderWithHighlightWord(parsedContent, highlightWord)}</em>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { escape } from "lodash-es";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const INLINE_CODE_REG = /`(.+?)`/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(INLINE_CODE_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
return `<code>${escape(matchResult[1])}</code>`;
|
||||
return `<code>${renderWithHighlightWord(escape(matchResult[1]), highlightWord)}</code>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -4,16 +4,20 @@ import Bold from "./Bold";
|
|||
import { marked } from "..";
|
||||
import InlineCode from "./InlineCode";
|
||||
import BoldEmphasis from "./BoldEmphasis";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const LINK_REG = /\[(.*?)\]\((.+?)\)+/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(LINK_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold]);
|
||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[2])}'>${parsedContent}</a>`;
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], [InlineCode, BoldEmphasis, Emphasis, Bold]);
|
||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[2])}'>${renderWithHighlightWord(
|
||||
parsedContent,
|
||||
highlightWord
|
||||
)}</a>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { marked } from "..";
|
|||
|
||||
export const ORDERED_LIST_REG = /^(\d+)\. (.+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(ORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[2], [], inlineElementParserList);
|
||||
const parsedContent = marked(matchResult[2], highlightWord, [], inlineElementParserList);
|
||||
return `<p><span class='ol-block'>${matchResult[1]}.</span>${parsedContent}</p>${matchResult[3]}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import { marked } from "..";
|
|||
|
||||
export const PARAGRAPH_REG = /^(.*)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(PARAGRAPH_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], inlineElementParserList);
|
||||
return `<p>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { escape } from "lodash-es";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(PLAIN_LINK_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[1])}'>${escape(matchResult[1])}</a>`;
|
||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[1])}'>${renderWithHighlightWord(
|
||||
escape(matchResult[1]),
|
||||
highlightWord
|
||||
)}</a>`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { escape } from "lodash-es";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const PLAIN_TEXT_REG = /(.+)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(PLAIN_TEXT_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
return `${escape(matchResult[1])}`;
|
||||
return `${renderWithHighlightWord(escape(matchResult[1]), highlightWord)}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
* | 1 | 2 | 3 |
|
||||
* | 4 | 5 | 6 |
|
||||
*/
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const TABLE_REG = /^(\|.*\|)(?:(?:\n(?:\|-*)+\|))((?:\n\|.*\|)+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(TABLE_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
|
|
@ -29,11 +31,11 @@ const renderer = (rawStr: string): string => {
|
|||
return `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${tableHeader.map((str) => `<th>${str}</th>`).join("")}
|
||||
${tableHeader.map((str) => `<th>${renderWithHighlightWord(str, highlightWord)}</th>`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tableBody.map((row) => `<tr>${row.map((str) => `<td>${str}</td>`).join("")}</tr>`).join("")}
|
||||
${tableBody.map((row) => `<tr>${row.map((str) => `<td>${renderWithHighlightWord(str, highlightWord)}</td>`).join("")}</tr>`).join("")}
|
||||
</tbody>
|
||||
</table>${matchResult[3]}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { escape } from "lodash-es";
|
||||
import { renderWithHighlightWord } from "./utils";
|
||||
|
||||
export const TAG_REG = /#([^\s#]+?) /;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(TAG_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
return `<span class='tag-span'>#${escape(matchResult[1])}</span> `;
|
||||
return `<span class='tag-span'>#${renderWithHighlightWord(escape(matchResult[1]), highlightWord)}</span> `;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { marked } from "..";
|
|||
|
||||
export const TODO_LIST_REG = /^- \[ \] (.+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(TODO_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], inlineElementParserList);
|
||||
return `<p><span class='todo-block todo' data-value='TODO'></span>${parsedContent}</p>${escape(matchResult[2])}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { marked } from "..";
|
|||
|
||||
export const UNORDERED_LIST_REG = /^[*-] (.+)(\n?)/;
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const renderer = (rawStr: string, highlightWord: string | undefined): string => {
|
||||
const matchResult = rawStr.match(UNORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||
const parsedContent = marked(matchResult[1], highlightWord, [], inlineElementParserList);
|
||||
return `<p><span class='ul-block'>•</span>${parsedContent}</p>${escape(matchResult[2])}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@ export const blockElementParserList = [
|
|||
UnorderedList,
|
||||
Paragraph,
|
||||
];
|
||||
// TODO 感觉会递归
|
||||
export const inlineElementParserList = [Image, BoldEmphasis, Bold, Emphasis, Link, InlineCode, PlainLink, Tag, PlainText];
|
||||
export const parserList = [...blockElementParserList, ...inlineElementParserList];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export const renderWithHighlightWord = (result: string, highlightWord: string | undefined) => {
|
||||
if (highlightWord) {
|
||||
const highlightReg = RegExp(highlightWord, "g");
|
||||
result = result.replace(highlightReg, `<span class="highlight-word">${highlightWord}</span>`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
@ -116,6 +116,10 @@
|
|||
.img {
|
||||
@apply max-w-xs;
|
||||
}
|
||||
|
||||
.highlight-word{
|
||||
@apply bg-yellow-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue