mirror of https://github.com/usememos/memos.git
fix: include plain URLs and tags in memo snippet generation (#5688)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
92d937b1aa
commit
3d4f793f97
|
|
@ -244,15 +244,20 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error)
|
|||
|
||||
lastNodeWasBlock = false
|
||||
|
||||
// Only extract plain text nodes
|
||||
if textNode, ok := n.(*gast.Text); ok {
|
||||
segment := textNode.Segment
|
||||
// Extract text from various node types
|
||||
switch node := n.(type) {
|
||||
case *gast.Text:
|
||||
segment := node.Segment
|
||||
buf.Write(segment.Value(content))
|
||||
|
||||
// Add space if this is a soft line break
|
||||
if textNode.SoftLineBreak() {
|
||||
if node.SoftLineBreak() {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
case *gast.AutoLink:
|
||||
buf.Write(node.URL(content))
|
||||
return gast.WalkSkipChildren, nil
|
||||
case *mast.TagNode:
|
||||
buf.WriteByte('#')
|
||||
buf.Write(node.Tag)
|
||||
}
|
||||
|
||||
// Stop walking if we've exceeded double the max length
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@ func TestGenerateSnippet(t *testing.T) {
|
|||
maxLength: 100,
|
||||
expected: "Item 1 Item 2 Item 3",
|
||||
},
|
||||
{
|
||||
name: "plain URL autolink",
|
||||
content: "https://usememos.com",
|
||||
maxLength: 100,
|
||||
expected: "https://usememos.com",
|
||||
},
|
||||
{
|
||||
name: "text with plain URL",
|
||||
content: "Check out https://usememos.com for more info.",
|
||||
maxLength: 100,
|
||||
expected: "Check out https://usememos.com for more info.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -103,6 +115,35 @@ func TestGenerateSnippet(t *testing.T) {
|
|||
assert.Equal(t, tt.expected, snippet)
|
||||
})
|
||||
}
|
||||
|
||||
// Test with tag extension enabled (matches production config).
|
||||
svcWithTags := NewService(WithTagExtension())
|
||||
tagTests := []struct {
|
||||
name string
|
||||
content string
|
||||
maxLength int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "tag only",
|
||||
content: "#todo",
|
||||
maxLength: 100,
|
||||
expected: "#todo",
|
||||
},
|
||||
{
|
||||
name: "text with tags",
|
||||
content: "Remember to #review the #code",
|
||||
maxLength: 100,
|
||||
expected: "Remember to #review the #code",
|
||||
},
|
||||
}
|
||||
for _, tt := range tagTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
snippet, err := svcWithTags.GenerateSnippet([]byte(tt.content), tt.maxLength)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, snippet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractProperties(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
|
|||
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
|
||||
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-foreground">Attachments ({items.length})</span>
|
||||
<span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
|
||||
</div>
|
||||
|
||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
|
|||
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
|
||||
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-foreground">Relations ({referenceRelations.length})</span>
|
||||
<span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
|
||||
</div>
|
||||
|
||||
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
|
|||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||
import { useMemoComments } from "@/hooks/useMemoQueries";
|
||||
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
|
||||
import MemoSnippetLink from "./MemoSnippetLink";
|
||||
|
||||
const MemoCommentListView: React.FC = () => {
|
||||
const { memo } = useMemoViewContext();
|
||||
|
|
@ -23,7 +24,7 @@ const MemoCommentListView: React.FC = () => {
|
|||
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
|
||||
<Link
|
||||
to={`/${memo.name}#comments`}
|
||||
className="flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground hover:underline underline-offset-2 transition-colors"
|
||||
className="flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors"
|
||||
>
|
||||
View all
|
||||
<ArrowUpRightIcon className="w-3 h-3" />
|
||||
|
|
@ -32,13 +33,13 @@ const MemoCommentListView: React.FC = () => {
|
|||
{displayedComments.map((comment) => {
|
||||
const uid = extractMemoIdFromName(comment.name);
|
||||
return (
|
||||
<Link
|
||||
<MemoSnippetLink
|
||||
key={comment.name}
|
||||
name={comment.name}
|
||||
snippet={comment.snippet || comment.content}
|
||||
to={`/${memo.name}#${uid}`}
|
||||
className="bg-muted/60 rounded-md px-2 py-1 text-xs text-muted-foreground truncate leading-relaxed hover:bg-muted transition-colors block"
|
||||
>
|
||||
{comment.content}
|
||||
</Link>
|
||||
className="bg-muted/40 rounded-md"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MemoSnippetLinkProps {
|
||||
name: string;
|
||||
snippet: string;
|
||||
to: string;
|
||||
state?: object;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MemoSnippetLink = ({ name, snippet, to, state, className }: MemoSnippetLinkProps) => {
|
||||
const memoId = extractMemoIdFromName(name);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
|
||||
className,
|
||||
)}
|
||||
to={to}
|
||||
viewTransition
|
||||
state={state}
|
||||
>
|
||||
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
|
||||
{memoId.slice(0, 6)}
|
||||
</span>
|
||||
<span className="truncate">{snippet}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoSnippetLink;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import MemoSnippetLink from "../MemoSnippetLink";
|
||||
|
||||
interface RelationCardProps {
|
||||
memo: MemoRelation_Memo;
|
||||
|
|
@ -10,23 +8,8 @@ interface RelationCardProps {
|
|||
}
|
||||
|
||||
const RelationCard = ({ memo, parentPage, className }: RelationCardProps) => {
|
||||
const memoId = extractMemoIdFromName(memo.name);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
|
||||
className,
|
||||
)}
|
||||
to={`/${memo.name}`}
|
||||
viewTransition
|
||||
state={{ from: parentPage }}
|
||||
>
|
||||
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
|
||||
{memoId.slice(0, 6)}
|
||||
</span>
|
||||
<span className="truncate">{memo.snippet}</span>
|
||||
</Link>
|
||||
<MemoSnippetLink name={memo.name} snippet={memo.snippet} to={`/${memo.name}`} state={{ from: parentPage }} className={className} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
|
|||
onClick={tab.onClick}
|
||||
className={cn(
|
||||
"text-xs px-0 py-0 transition-colors",
|
||||
tab.active ? "text-foreground" : "text-muted-foreground hover:text-foreground",
|
||||
tab.active ? "text-muted-foreground" : "text-muted-foreground/60 hover:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{tab.label} ({tab.count})
|
||||
|
|
@ -37,7 +37,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-foreground">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{title} ({count})
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue