fix(mobile): restore spotlight highlights
This commit is contained in:
parent
e3227c932f
commit
f5158ee6cc
|
|
@ -54,7 +54,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>3</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useMemo } from "react"
|
||||
import type { TextProps } from "react-native"
|
||||
import { View } from "react-native"
|
||||
import { Text as RNText, View } from "react-native"
|
||||
|
||||
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
|
||||
import { useSpotlightSettingKey } from "@/src/atoms/settings/spotlight"
|
||||
import { Text } from "@/src/components/ui/typography/Text"
|
||||
import { HighlightedText } from "@/src/modules/spotlight/HighlightedText"
|
||||
|
||||
export const EntryTranslation = ({
|
||||
source,
|
||||
|
|
@ -22,6 +24,7 @@ export const EntryTranslation = ({
|
|||
bilingual?: boolean
|
||||
} & TextProps) => {
|
||||
const bilingualFinal = useGeneralSettingKey("translationMode") === "bilingual" || bilingual
|
||||
const spotlightRules = useSpotlightSettingKey("spotlights")
|
||||
const nextSource = useMemo(() => {
|
||||
if (!source) {
|
||||
return ""
|
||||
|
|
@ -37,25 +40,27 @@ export const EntryTranslation = ({
|
|||
if (!bilingualFinal) {
|
||||
return (
|
||||
<Text {...props} className={className}>
|
||||
{nextTarget || nextSource}
|
||||
<HighlightedText text={nextTarget || nextSource} rules={spotlightRules} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
if (inline) {
|
||||
return (
|
||||
<Text {...props} className={className}>
|
||||
{`${nextTarget ? `${nextTarget} ⇋ ` : ""}${nextSource}`}
|
||||
{nextTarget ? <HighlightedText text={nextTarget} rules={spotlightRules} /> : null}
|
||||
{nextTarget ? <RNText>{" ⇋ "}</RNText> : null}
|
||||
<HighlightedText text={nextSource} rules={spotlightRules} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<Text {...props} className={className}>
|
||||
{nextSource}
|
||||
<HighlightedText text={nextSource} rules={spotlightRules} />
|
||||
</Text>
|
||||
{nextTarget && (
|
||||
<Text {...props} className={className}>
|
||||
{nextTarget}
|
||||
<HighlightedText text={nextTarget} rules={spotlightRules} />
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import type { SpotlightRule } from "@follow/shared/spotlight"
|
||||
import { Fragment, useMemo } from "react"
|
||||
import { Text as RNText } from "react-native"
|
||||
|
||||
import { getHighlightedTextSegments } from "./highlightedTextSegments"
|
||||
import { getHighlightedTextSegmentStyle } from "./highlightedTextStyle"
|
||||
|
||||
export const HighlightedText = ({
|
||||
text,
|
||||
rules,
|
||||
}: {
|
||||
text?: string | null
|
||||
rules: SpotlightRule[]
|
||||
}) => {
|
||||
const segments = useMemo(() => getHighlightedTextSegments(text, rules), [rules, text])
|
||||
const keyedSegments = useMemo(() => {
|
||||
let offset = 0
|
||||
|
||||
return segments.map((segment) => {
|
||||
const key = segment.highlight ? `${offset}-${segment.highlight.ruleId}-${segment.text}` : null
|
||||
offset += segment.text.length
|
||||
return { key: key ?? `${offset}-${segment.text}`, segment }
|
||||
})
|
||||
}, [segments])
|
||||
|
||||
return (
|
||||
<>
|
||||
{keyedSegments.map(({ key, segment }) =>
|
||||
segment.highlight ? (
|
||||
<RNText key={key} style={getHighlightedTextSegmentStyle(segment.highlight.color)}>
|
||||
{segment.text}
|
||||
</RNText>
|
||||
) : (
|
||||
<Fragment key={key}>{segment.text}</Fragment>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createStore, Provider, useAtomValue } from "jotai"
|
||||
|
||||
import { entryAtom, noMediaAtom, readerRenderInlineStyleAtom } from "./atoms"
|
||||
import { entryAtom, noMediaAtom, readerRenderInlineStyleAtom, spotlightAtom } from "./atoms"
|
||||
import { HTML } from "./HTML"
|
||||
import { WebViewBridgeManager } from "./managers/webview-bridge"
|
||||
|
||||
|
|
@ -14,10 +14,15 @@ export const App = () => {
|
|||
const entry = useAtomValue(entryAtom, { store })
|
||||
const readerRenderInlineStyle = useAtomValue(readerRenderInlineStyleAtom, { store })
|
||||
const noMedia = useAtomValue(noMediaAtom, { store })
|
||||
const spotlightRules = useAtomValue(spotlightAtom, { store })
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<HTML renderInlineStyle={readerRenderInlineStyle} noMedia={noMedia}>
|
||||
<HTML
|
||||
renderInlineStyle={readerRenderInlineStyle}
|
||||
noMedia={noMedia}
|
||||
spotlightRules={spotlightRules}
|
||||
>
|
||||
{entry?.content}
|
||||
</HTML>
|
||||
</Provider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.js"
|
||||
import type { SpotlightRule } from "@follow/shared/spotlight"
|
||||
import { clsx } from "clsx"
|
||||
import katexStyle from "katex/dist/katex.min.css?raw"
|
||||
import * as React from "react"
|
||||
|
|
@ -14,6 +15,7 @@ export type HTMLProps<A extends keyof React.JSX.IntrinsicElements = "div"> = {
|
|||
|
||||
accessory?: React.ReactNode
|
||||
noMedia?: boolean
|
||||
spotlightRules?: SpotlightRule[]
|
||||
} & React.JSX.IntrinsicElements[A] &
|
||||
Partial<{
|
||||
renderInlineStyle: boolean
|
||||
|
|
@ -25,25 +27,29 @@ export const HTML = <A extends keyof React.JSX.IntrinsicElements = "div">(props:
|
|||
as = "article",
|
||||
accessory,
|
||||
noMedia,
|
||||
spotlightRules,
|
||||
|
||||
...rest
|
||||
} = props
|
||||
const [remarkOptions, setRemarkOptions] = useState({
|
||||
renderInlineStyle,
|
||||
noMedia,
|
||||
spotlightRules,
|
||||
})
|
||||
const [shouldForceReMountKey, setShouldForceReMountKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setRemarkOptions((options) => {
|
||||
if (JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia })) {
|
||||
if (
|
||||
JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia, spotlightRules })
|
||||
) {
|
||||
return options
|
||||
}
|
||||
|
||||
setShouldForceReMountKey((key) => key + 1)
|
||||
return { ...options, renderInlineStyle, noMedia }
|
||||
return { ...options, renderInlineStyle, noMedia, spotlightRules }
|
||||
})
|
||||
}, [renderInlineStyle, noMedia])
|
||||
}, [renderInlineStyle, noMedia, spotlightRules])
|
||||
|
||||
const [refElement, setRefElement] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.jsx"
|
||||
import { Checkbox } from "@follow/components/ui/checkbox/index.jsx"
|
||||
import type { SpotlightRule } from "@follow/shared/spotlight"
|
||||
import { parseHtml as parseHtmlGeneral } from "@follow/utils/html"
|
||||
import type { Components } from "hast-util-to-jsx-runtime"
|
||||
import { createElement } from "react"
|
||||
|
|
@ -9,8 +10,9 @@ import { createHeadingRenderer, MarkdownLink, MarkdownP } from "./components"
|
|||
import { MarkdownImage } from "./components/image"
|
||||
import { Math } from "./components/math"
|
||||
import { ShikiHighLighter } from "./components/shiki"
|
||||
import { applySpotlightToHtmlRendererTree } from "./spotlight"
|
||||
|
||||
const Style: Components["style"] = ({ node, ...props }) => {
|
||||
const renderStyleTag: Components["style"] = ({ node, ...props }) => {
|
||||
if (typeof props.children === "string") {
|
||||
return createElement(MemoedDangerousHTMLStyle, null, props.children)
|
||||
}
|
||||
|
|
@ -22,10 +24,16 @@ export const parseHtml = (
|
|||
options?: Partial<{
|
||||
renderInlineStyle: boolean
|
||||
noMedia?: boolean
|
||||
spotlightRules?: SpotlightRule[]
|
||||
}>,
|
||||
) => {
|
||||
const spotlightRules = options?.spotlightRules ?? []
|
||||
|
||||
return parseHtmlGeneral(content, {
|
||||
...options,
|
||||
hastTransform: (tree) => {
|
||||
applySpotlightToHtmlRendererTree(tree, spotlightRules)
|
||||
},
|
||||
components: {
|
||||
a: ({ node, ...props }) => {
|
||||
// Ignore link wrapper when child is an image to ensure image preview
|
||||
|
|
@ -62,7 +70,7 @@ export const parseHtml = (
|
|||
h6: (props) => {
|
||||
return createHeadingRenderer(6)(props)
|
||||
},
|
||||
style: Style,
|
||||
style: renderStyleTag,
|
||||
img: ({ node, ...props }) => {
|
||||
return createElement(MarkdownImage, props as any)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest"
|
|||
|
||||
import { entryAtom, spotlightAtom } from "./atoms"
|
||||
import { WebViewBridgeManager } from "./managers/webview-bridge"
|
||||
import { parseHtml } from "./parser"
|
||||
import { applySpotlightToHtmlRendererTree } from "./spotlight"
|
||||
|
||||
const spotlightRules: SpotlightRule[] = [
|
||||
|
|
@ -212,3 +213,30 @@ describe("WebViewBridgeManager", () => {
|
|||
expect(store.get(spotlightAtom)).toEqual(spotlightRules)
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseHtml", () => {
|
||||
test("injects spotlight markup when parser receives spotlight rules", () => {
|
||||
const { hastTree } = parseHtml("<p>alpha beta</p>", { spotlightRules })
|
||||
|
||||
expect(hastTree).toMatchObject({
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "p",
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: expect.objectContaining({
|
||||
"data-spotlight-rule-id": "alpha-rule",
|
||||
style: "background-color:#ffcc00CC;border-radius:5px;padding-inline:2px;",
|
||||
}),
|
||||
children: [{ type: "text", value: "alpha" }],
|
||||
},
|
||||
{ type: "text", value: " beta" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue