chore: remove unused syntax

- Removed the wikilink extension from markdown services in test and API service.
- Deleted the DefaultLink and WikiLink components, simplifying link handling.
- Updated ConditionalComponent to remove wikilink checks.
- Adjusted MemoContent to exclude wikilink handling in markdown rendering.
- Refined markdown styles for compact rendering, enhancing readability.
- Added a Markdown Styling Guide to document the new compact styling approach.
pull/5122/merge
Claude 1 week ago
parent 7eec424274
commit 596b894ca0

@ -1,32 +0,0 @@
package ast
import (
gast "github.com/yuin/goldmark/ast"
)
// WikilinkNode represents [[target]] or [[target?params]] syntax.
type WikilinkNode struct {
gast.BaseInline
// Target is the link destination (e.g., "memos/1", "Hello world", "resources/101")
Target []byte
// Params are optional parameters (e.g., "align=center" from [[target?align=center]])
Params []byte
}
// KindWikilink is the NodeKind for WikilinkNode.
var KindWikilink = gast.NewNodeKind("Wikilink")
// Kind returns KindWikilink.
func (*WikilinkNode) Kind() gast.NodeKind {
return KindWikilink
}
// Dump implements Node.Dump for debugging.
func (n *WikilinkNode) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, map[string]string{
"Target": string(n.Target),
"Params": string(n.Params),
}, nil)
}

@ -1,24 +0,0 @@
package extensions
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util"
mparser "github.com/usememos/memos/plugin/markdown/parser"
)
type wikilinkExtension struct{}
// WikilinkExtension is a goldmark extension for [[...]] wikilink syntax.
var WikilinkExtension = &wikilinkExtension{}
// Extend extends the goldmark parser with wikilink support.
func (*wikilinkExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
// Priority 199 - run before standard link parser (500) but after tags (200)
util.Prioritized(mparser.NewWikilinkParser(), 199),
),
)
}

@ -62,8 +62,7 @@ type service struct {
type Option func(*config)
type config struct {
enableTags bool
enableWikilink bool
enableTags bool
}
// WithTagExtension enables #tag parsing.
@ -73,13 +72,6 @@ func WithTagExtension() Option {
}
}
// WithWikilinkExtension enables [[wikilink]] parsing.
func WithWikilinkExtension() Option {
return func(c *config) {
c.enableWikilink = true
}
}
// NewService creates a new markdown service with the given options.
func NewService(opts ...Option) Service {
cfg := &config{}
@ -95,9 +87,6 @@ func NewService(opts ...Option) Service {
if cfg.enableTags {
exts = append(exts, extensions.TagExtension)
}
if cfg.enableWikilink {
exts = append(exts, extensions.WikilinkExtension)
}
md := goldmark.New(
goldmark.WithExtensions(exts...),
@ -164,7 +153,7 @@ func (s *service) ExtractProperties(content []byte) (*storepb.MemoPayload_Proper
}
switch n.Kind() {
case gast.KindLink, mast.KindWikilink:
case gast.KindLink:
prop.HasLink = true
case gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan:
@ -321,7 +310,7 @@ func (s *service) ExtractAll(content []byte) (*ExtractedData, error) {
// Extract properties based on node kind
switch n.Kind() {
case gast.KindLink, mast.KindWikilink:
case gast.KindLink:
data.Property.HasLink = true
case gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan:

@ -109,7 +109,6 @@ func TestExtractProperties(t *testing.T) {
tests := []struct {
name string
content string
withExt bool
hasLink bool
hasCode bool
hasTasks bool
@ -118,7 +117,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "plain text",
content: "Just plain text",
withExt: false,
hasLink: false,
hasCode: false,
hasTasks: false,
@ -127,7 +125,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "with link",
content: "Check out [this link](https://example.com)",
withExt: false,
hasLink: true,
hasCode: false,
hasTasks: false,
@ -136,7 +133,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "with inline code",
content: "Use `console.log()` to debug",
withExt: false,
hasLink: false,
hasCode: true,
hasTasks: false,
@ -145,7 +141,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "with code block",
content: "```go\nfunc main() {}\n```",
withExt: false,
hasLink: false,
hasCode: true,
hasTasks: false,
@ -154,7 +149,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "with completed task",
content: "- [x] Completed task",
withExt: false,
hasLink: false,
hasCode: false,
hasTasks: true,
@ -163,7 +157,6 @@ func TestExtractProperties(t *testing.T) {
{
name: "with incomplete task",
content: "- [ ] Todo item",
withExt: false,
hasLink: false,
hasCode: false,
hasTasks: true,
@ -172,25 +165,14 @@ func TestExtractProperties(t *testing.T) {
{
name: "mixed tasks",
content: "- [x] Done\n- [ ] Not done",
withExt: false,
hasLink: false,
hasCode: false,
hasTasks: true,
hasInc: true,
},
{
name: "with referenced content",
content: "See [[memos/1]] for details",
withExt: true,
hasLink: true,
hasCode: false,
hasTasks: false,
hasInc: false,
},
{
name: "everything",
content: "# Title\n\n[Link](url)\n\n`code`\n\n- [ ] Task",
withExt: false,
hasLink: true,
hasCode: true,
hasTasks: true,
@ -200,12 +182,7 @@ func TestExtractProperties(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var svc Service
if tt.withExt {
svc = NewService(WithWikilinkExtension())
} else {
svc = NewService()
}
svc := NewService()
props, err := svc.ExtractProperties([]byte(tt.content))
require.NoError(t, err)

@ -45,7 +45,7 @@ func (*tagParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.N
}
// Scan tag characters
// Valid: alphanumeric, dash, underscore
// Valid: alphanumeric, dash, underscore, forward slash
tagEnd := 1 // Start after #
for tagEnd < len(line) {
c := line[tagEnd]
@ -53,7 +53,7 @@ func (*tagParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.N
isValid := (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' || c == '_'
c == '-' || c == '_' || c == '/'
if !isValid {
break

@ -96,6 +96,36 @@ func TestTagParser(t *testing.T) {
expectedTag: "WorkNotes",
shouldParse: true,
},
{
name: "hierarchical tag with slash",
input: "#tag1/subtag",
expectedTag: "tag1/subtag",
shouldParse: true,
},
{
name: "hierarchical tag with multiple levels",
input: "#tag1/subtag/subtag2",
expectedTag: "tag1/subtag/subtag2",
shouldParse: true,
},
{
name: "hierarchical tag followed by space",
input: "#work/notes ",
expectedTag: "work/notes",
shouldParse: true,
},
{
name: "hierarchical tag followed by punctuation",
input: "#project/2024.",
expectedTag: "project/2024",
shouldParse: true,
},
{
name: "hierarchical tag with numbers and dashes",
input: "#work-log/2024/q1",
expectedTag: "work-log/2024/q1",
shouldParse: true,
},
}
for _, tt := range tests {

@ -1,104 +0,0 @@
package parser
import (
"bytes"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
mast "github.com/usememos/memos/plugin/markdown/ast"
)
type wikilinkParser struct{}
// NewWikilinkParser creates a new inline parser for [[...]] wikilink syntax.
func NewWikilinkParser() parser.InlineParser {
return &wikilinkParser{}
}
// Trigger returns the characters that trigger this parser.
func (*wikilinkParser) Trigger() []byte {
return []byte{'['}
}
// Parse parses [[target]] or [[target?params]] wikilink syntax.
func (*wikilinkParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.Node {
line, _ := block.PeekLine()
// Must start with [[
if len(line) < 2 || line[0] != '[' || line[1] != '[' {
return nil
}
// Find closing ]]
closePos := findClosingBrackets(line[2:])
if closePos == -1 {
return nil
}
// Extract content between [[ and ]]
// closePos is relative to line[2:], so actual position is closePos + 2
contentStart := 2
contentEnd := contentStart + closePos
content := line[contentStart:contentEnd]
// Empty content is not allowed
if len(bytes.TrimSpace(content)) == 0 {
return nil
}
// Parse target and parameters
target, params := parseTargetAndParams(content)
// Advance reader position
// +2 for [[, +len(content), +2 for ]]
block.Advance(contentEnd + 2)
// Create AST node
node := &mast.WikilinkNode{
Target: target,
Params: params,
}
return node
}
// findClosingBrackets finds the position of ]] in the byte slice.
// Returns -1 if not found.
func findClosingBrackets(data []byte) int {
for i := 0; i < len(data)-1; i++ {
if data[i] == ']' && data[i+1] == ']' {
return i
}
}
return -1
}
// parseTargetAndParams splits content on ? to extract target and parameters.
func parseTargetAndParams(content []byte) (target []byte, params []byte) {
// Find ? separator
idx := bytes.IndexByte(content, '?')
if idx == -1 {
// No parameters
target = bytes.TrimSpace(content)
return target, nil
}
// Split on ?
target = bytes.TrimSpace(content[:idx])
params = content[idx+1:] // Keep params as-is (don't trim, might have meaningful spaces)
// Make copies to avoid issues with slice sharing
targetCopy := make([]byte, len(target))
copy(targetCopy, target)
var paramsCopy []byte
if len(params) > 0 {
paramsCopy = make([]byte, len(params))
copy(paramsCopy, params)
}
return targetCopy, paramsCopy
}

@ -1,252 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
mast "github.com/usememos/memos/plugin/markdown/ast"
)
func TestWikilinkParser(t *testing.T) {
tests := []struct {
name string
input string
expectedTarget string
expectedParams string
shouldParse bool
}{
{
name: "basic wikilink",
input: "[[Hello world]]",
expectedTarget: "Hello world",
expectedParams: "",
shouldParse: true,
},
{
name: "memo wikilink",
input: "[[memos/1]]",
expectedTarget: "memos/1",
expectedParams: "",
shouldParse: true,
},
{
name: "resource wikilink",
input: "[[resources/101]]",
expectedTarget: "resources/101",
expectedParams: "",
shouldParse: true,
},
{
name: "with parameters",
input: "[[resources/101?align=center]]",
expectedTarget: "resources/101",
expectedParams: "align=center",
shouldParse: true,
},
{
name: "multiple parameters",
input: "[[resources/101?align=center&width=300]]",
expectedTarget: "resources/101",
expectedParams: "align=center&width=300",
shouldParse: true,
},
{
name: "inline with text after",
input: "[[resources/101]]111",
expectedTarget: "resources/101",
expectedParams: "",
shouldParse: true,
},
{
name: "whitespace trimmed",
input: "[[ Hello world ]]",
expectedTarget: "Hello world",
expectedParams: "",
shouldParse: true,
},
{
name: "empty content",
input: "[[]]",
expectedTarget: "",
expectedParams: "",
shouldParse: false,
},
{
name: "whitespace only",
input: "[[ ]]",
expectedTarget: "",
expectedParams: "",
shouldParse: false,
},
{
name: "missing closing brackets",
input: "[[Hello world",
expectedTarget: "",
expectedParams: "",
shouldParse: false,
},
{
name: "single bracket",
input: "[Hello]",
expectedTarget: "",
expectedParams: "",
shouldParse: false,
},
{
name: "nested brackets",
input: "[[outer [[inner]] ]]",
expectedTarget: "outer [[inner",
expectedParams: "",
shouldParse: true, // Stops at first ]]
},
{
name: "special characters",
input: "[[Project/2024/Notes]]",
expectedTarget: "Project/2024/Notes",
expectedParams: "",
shouldParse: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewWikilinkParser()
reader := text.NewReader([]byte(tt.input))
ctx := parser.NewContext()
node := p.Parse(nil, reader, ctx)
if tt.shouldParse {
require.NotNil(t, node, "Expected wikilink to be parsed")
require.IsType(t, &mast.WikilinkNode{}, node)
wikilinkNode, ok := node.(*mast.WikilinkNode)
require.True(t, ok, "Expected node to be *mast.WikilinkNode")
assert.Equal(t, tt.expectedTarget, string(wikilinkNode.Target))
assert.Equal(t, tt.expectedParams, string(wikilinkNode.Params))
} else {
assert.Nil(t, node, "Expected wikilink NOT to be parsed")
}
})
}
}
func TestWikilinkParser_Trigger(t *testing.T) {
p := NewWikilinkParser()
triggers := p.Trigger()
assert.Equal(t, []byte{'['}, triggers)
}
func TestFindClosingBrackets(t *testing.T) {
tests := []struct {
name string
input []byte
expected int
}{
{
name: "simple case",
input: []byte("hello]]world"),
expected: 5,
},
{
name: "not found",
input: []byte("hello world"),
expected: -1,
},
{
name: "at start",
input: []byte("]]hello"),
expected: 0,
},
{
name: "single bracket",
input: []byte("hello]world"),
expected: -1,
},
{
name: "empty",
input: []byte(""),
expected: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := findClosingBrackets(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseTargetAndParams(t *testing.T) {
tests := []struct {
name string
input []byte
expectedTarget string
expectedParams string
}{
{
name: "no params",
input: []byte("target"),
expectedTarget: "target",
expectedParams: "",
},
{
name: "with params",
input: []byte("target?param=value"),
expectedTarget: "target",
expectedParams: "param=value",
},
{
name: "multiple params",
input: []byte("target?a=1&b=2"),
expectedTarget: "target",
expectedParams: "a=1&b=2",
},
{
name: "whitespace trimmed from target",
input: []byte(" target ?param=value"),
expectedTarget: "target",
expectedParams: "param=value",
},
{
name: "empty params",
input: []byte("target?"),
expectedTarget: "target",
expectedParams: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target, params := parseTargetAndParams(tt.input)
assert.Equal(t, tt.expectedTarget, string(target))
assert.Equal(t, tt.expectedParams, string(params))
})
}
}
func TestWikilinkNode_Kind(t *testing.T) {
node := &mast.WikilinkNode{
Target: []byte("test"),
}
assert.Equal(t, mast.KindWikilink, node.Kind())
}
func TestWikilinkNode_Dump(t *testing.T) {
node := &mast.WikilinkNode{
Target: []byte("test"),
Params: []byte("param=value"),
}
// Should not panic
assert.NotPanics(t, func() {
node.Dump([]byte("[[test?param=value]]"), 0)
})
}

@ -156,15 +156,6 @@ func (r *MarkdownRenderer) renderNode(node gast.Node, source []byte, depth int)
r.buf.WriteByte('#')
r.buf.Write(n.Tag)
case *mast.WikilinkNode:
r.buf.WriteString("[[")
r.buf.Write(n.Target)
if len(n.Params) > 0 {
r.buf.WriteByte('?')
r.buf.Write(n.Params)
}
r.buf.WriteString("]]")
default:
// For unknown nodes, try to render children
r.renderChildren(n, source, depth)

@ -19,7 +19,6 @@ func TestMarkdownRenderer(t *testing.T) {
goldmark.WithExtensions(
extension.GFM,
extensions.TagExtension,
extensions.WikilinkExtension,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
@ -111,15 +110,10 @@ func TestMarkdownRenderer(t *testing.T) {
input: "#work #important meeting notes",
expected: "#work #important meeting notes",
},
{
name: "referenced content (wikilink)",
input: "Check [[memos/42]] for details",
expected: "Check [[memos/42]] for details",
},
{
name: "complex mixed content",
input: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\nSee [[memos/1]] for background.\n\n```python\nprint('hello')\n```",
expected: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\nSee [[memos/1]] for background.\n\n```python\nprint('hello')\n```",
input: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\n```python\nprint('hello')\n```",
expected: "# Meeting Notes\n\n**Date**: 2024-01-01\n\n## Attendees\n\n- Alice\n- Bob\n\n## Discussion\n\nWe discussed #project status.\n\n```python\nprint('hello')\n```",
},
}
@ -153,7 +147,6 @@ func TestMarkdownRendererPreservesStructure(t *testing.T) {
goldmark.WithExtensions(
extension.GFM,
extensions.TagExtension,
extensions.WikilinkExtension,
),
)
@ -162,7 +155,6 @@ func TestMarkdownRendererPreservesStructure(t *testing.T) {
"**Bold** and *italic*",
"- List\n- Items",
"#tag #another",
"[[wikilink]]",
"> Quote",
}

@ -39,7 +39,6 @@ func NewTestService(t *testing.T) *TestService {
secret := "test-secret"
markdownService := markdown.NewService(
markdown.WithTagExtension(),
markdown.WithWikilinkExtension(),
)
service := &apiv1.APIV1Service{
Secret: secret,

@ -45,7 +45,6 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store
grpc.EnableTracing = true
markdownService := markdown.NewService(
markdown.WithTagExtension(),
markdown.WithWikilinkExtension(),
)
apiv1Service := &APIV1Service{
Secret: secret,

@ -0,0 +1,153 @@
# Markdown Styling Guide
This document describes the compact markdown styling approach used in this codebase.
## Design Principles
Our markdown rendering uses compact spacing optimized for memos and notes:
### 1. **Scoped Styles**
All markdown styles are scoped to `.markdown-content` to avoid global pollution:
```css
.markdown-content p { /* scoped */ }
```
### 2. **Compact Block Spacing**
All block elements use **8px (0.5rem)** bottom margin:
- Paragraphs
- Lists (ul, ol)
- Code blocks (pre)
- Blockquotes
- Tables
- Horizontal rules
This is more compact than GitHub's standard (16px) but maintains readability for memo-style content.
### 3. **First/Last Child Normalization**
```css
.markdown-content > :first-child {
margin-top: 0 !important;
}
.markdown-content > :last-child {
margin-bottom: 0 !important;
}
```
This prevents double margins at container boundaries.
### 4. **Nested Element Spacing**
Nested elements (lists within lists, paragraphs within lists) use **minimal spacing** (2px/0.125rem):
```css
.markdown-content li > ul {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
```
### 5. **Heading Separation**
Headings have moderate top margins (12px/0.75rem) to create visual sections:
```css
.markdown-content h1,
.markdown-content h2 {
margin-top: 0.75rem;
margin-bottom: 0.5rem;
}
```
### 6. **No White-Space Preservation**
We do NOT use `white-space: pre-line`. Spacing is controlled entirely by CSS margins, matching how GitHub/ChatGPT/Claude work.
## Component Architecture
We use a **hybrid approach**:
### CSS-Based (for standard elements)
```tsx
<div className="markdown-content">
<ReactMarkdown>
{content}
</ReactMarkdown>
</div>
```
Standard elements (p, ul, ol, h1-h6, etc.) are styled via CSS.
### Component-Based (for custom elements)
```tsx
<ReactMarkdown
components={{
input: TaskListItem, // Custom task list checkboxes
span: Tag, // Custom #tag rendering
}}
>
{content}
</ReactMarkdown>
```
Custom elements use React components for interactivity.
## Comparison with Industry Standards
| Feature | GitHub | ChatGPT | Claude | Memos (ours) |
|---------|--------|---------|--------|--------------|
| Block margin | 16px | 16px | 16px | 8px (compact) ⚡ |
| Scoped styles | `.markdown-body` | `.prose` | Custom | `.markdown-content` ✅ |
| First/last normalization | ✅ | ✅ | ✅ | ✅ |
| Heading underlines (h1/h2) | ✅ | ❌ | ❌ | ✅ |
| Custom components | Few | Many | Many | Some ✅ |
| Line height | 1.6 | 1.6 | 1.6 | 1.5 (compact) ⚡ |
| List padding | 2em | 2em | 2em | 1.5em (compact) ⚡ |
| Code block padding | 16px | 16px | 16px | 8-12px (compact) ⚡ |
**Note:** Our compact spacing is optimized for memo/note-taking apps where screen real estate is important.
## Examples
### Input
```markdown
1312
* 123123
```
### Rendering
- Paragraph "1312" with `margin-bottom: 0.5rem` (8px)
- List with `margin-top: 0` (normalized)
- Result: Single 8px gap between paragraph and list ✅
### Before (with `white-space: pre-line`)
```
1312
[blank line from preserved \n\n]
[16px margin]
* 123123
```
Result: Double spacing ❌
### After (compact spacing, no white-space preservation)
```
1312
[8px margin only]
* 123123
```
Result: Clean, compact single spacing ✅
## Testing
To verify correct rendering:
1. **Text followed by list**: `"text\n\n* item"` → single 8px gap
2. **List followed by text**: `"* item\n\ntext"` → single 8px gap
3. **Nested lists**: Should have minimal spacing (2px)
4. **Headings**: Should have 12px top margin (except first child)
5. **Blockquotes**: Should handle nested content properly
6. **Code blocks**: Should have 8-12px padding (compact)
7. **Tables**: Should have compact cell padding (4px vertical, 8px horizontal)
## References
- [CommonMark Spec](https://spec.commonmark.org/)
- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/)
- [GitHub Markdown CSS](https://github.com/sindresorhus/github-markdown-css)
- [Tailwind Typography](https://tailwindcss.com/docs/typography-plugin)

@ -56,7 +56,6 @@
"react-use": "^17.6.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-wiki-link": "^2.0.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13",
"textarea-caret": "^3.1.0",

@ -152,9 +152,6 @@ importers:
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
remark-wiki-link:
specifier: ^2.0.1
version: 2.0.1
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
@ -1748,21 +1745,12 @@ packages:
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
character-entities-legacy@1.1.4:
resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
character-entities-legacy@3.0.0:
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
character-entities@1.2.4:
resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==}
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
character-reference-invalid@1.1.4:
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
@ -2494,15 +2482,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
is-alphabetical@1.0.4:
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
is-alphanumerical@1.0.4:
resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==}
is-alphanumerical@2.0.1:
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
@ -2541,9 +2523,6 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
is-decimal@1.0.4:
resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==}
is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
@ -2563,9 +2542,6 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-hexadecimal@1.0.4:
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
@ -2811,9 +2787,6 @@ packages:
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
longest-streak@2.0.4:
resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@ -2883,21 +2856,12 @@ packages:
mdast-util-to-hast@13.2.0:
resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
mdast-util-to-markdown@0.6.5:
resolution: {integrity: sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==}
mdast-util-to-markdown@2.1.2:
resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
mdast-util-to-string@2.0.0:
resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==}
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdast-util-wiki-link@0.1.2:
resolution: {integrity: sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg==}
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
@ -2932,9 +2896,6 @@ packages:
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
micromark-extension-wiki-link@0.0.4:
resolution: {integrity: sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw==}
micromark-factory-destination@2.0.1:
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
@ -3120,9 +3081,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-entities@2.0.0:
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
@ -3372,13 +3330,6 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remark-wiki-link@2.0.1:
resolution: {integrity: sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA==}
repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
@ -3878,9 +3829,6 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zwitch@1.0.5:
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -5404,16 +5352,10 @@ snapshots:
character-entities-html4@2.1.0: {}
character-entities-legacy@1.1.4: {}
character-entities-legacy@3.0.0: {}
character-entities@1.2.4: {}
character-entities@2.0.2: {}
character-reference-invalid@1.1.4: {}
character-reference-invalid@2.0.1: {}
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
@ -6345,15 +6287,8 @@ snapshots:
internmap@2.0.3: {}
is-alphabetical@1.0.4: {}
is-alphabetical@2.0.1: {}
is-alphanumerical@1.0.4:
dependencies:
is-alphabetical: 1.0.4
is-decimal: 1.0.4
is-alphanumerical@2.0.1:
dependencies:
is-alphabetical: 2.0.1
@ -6401,8 +6336,6 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-decimal@1.0.4: {}
is-decimal@2.0.1: {}
is-extglob@2.1.1: {}
@ -6422,8 +6355,6 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-hexadecimal@1.0.4: {}
is-hexadecimal@2.0.1: {}
is-map@2.0.3: {}
@ -6630,8 +6561,6 @@ snapshots:
long@5.3.2: {}
longest-streak@2.0.4: {}
longest-streak@3.1.0: {}
loose-envify@1.4.0:
@ -6793,15 +6722,6 @@ snapshots:
unist-util-visit: 5.0.0
vfile: 6.0.3
mdast-util-to-markdown@0.6.5:
dependencies:
'@types/unist': 2.0.11
longest-streak: 2.0.4
mdast-util-to-string: 2.0.0
parse-entities: 2.0.0
repeat-string: 1.6.1
zwitch: 1.0.5
mdast-util-to-markdown@2.1.2:
dependencies:
'@types/mdast': 4.0.4
@ -6814,17 +6734,10 @@ snapshots:
unist-util-visit: 5.0.0
zwitch: 2.0.4
mdast-util-to-string@2.0.0: {}
mdast-util-to-string@4.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-wiki-link@0.1.2:
dependencies:
'@babel/runtime': 7.28.4
mdast-util-to-markdown: 0.6.5
mdn-data@2.0.14: {}
merge2@1.4.1: {}
@ -6931,10 +6844,6 @@ snapshots:
micromark-util-combine-extensions: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-wiki-link@0.0.4:
dependencies:
'@babel/runtime': 7.28.4
micromark-factory-destination@2.0.1:
dependencies:
micromark-util-character: 2.1.1
@ -7188,15 +7097,6 @@ snapshots:
dependencies:
callsites: 3.1.0
parse-entities@2.0.0:
dependencies:
character-entities: 1.2.4
character-entities-legacy: 1.1.4
character-reference-invalid: 1.1.4
is-alphanumerical: 1.0.4
is-decimal: 1.0.4
is-hexadecimal: 1.0.4
parse-entities@4.0.2:
dependencies:
'@types/unist': 2.0.11
@ -7487,14 +7387,6 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
remark-wiki-link@2.0.1:
dependencies:
'@babel/runtime': 7.28.4
mdast-util-wiki-link: 0.1.2
micromark-extension-wiki-link: 0.0.4
repeat-string@1.6.1: {}
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
@ -8052,6 +7944,4 @@ snapshots:
yocto-queue@0.1.0: {}
zwitch@1.0.5: {}
zwitch@2.0.4: {}

@ -39,15 +39,6 @@ export const createConditionalComponent = <P extends Record<string, any>>(
* - First checks node.data.mdastType (preserved by remarkPreserveType plugin)
* - Falls back to checking HAST properties/className for compatibility
*/
export const isWikiLinkNode = (node: any): boolean => {
// Check preserved mdast type first
if (node?.data?.mdastType === "wikiLink") {
return true;
}
// Fallback: check hast properties
return node?.properties?.className?.includes?.("wikilink") || false;
};
export const isTagNode = (node: any): boolean => {
// Check preserved mdast type first
if (node?.data?.mdastType === "tagNode") {

@ -1,30 +0,0 @@
import React from "react";
/**
* Default link component for regular markdown links
*
* Handles external links with proper target and rel attributes.
*/
interface DefaultLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
node?: any; // AST node from react-markdown
href?: string;
children?: React.ReactNode;
}
export const DefaultLink: React.FC<DefaultLinkProps> = ({ href, children, ...props }) => {
const isExternal = href?.startsWith("http://") || href?.startsWith("https://");
return (
<a
{...props}
href={href}
className="text-primary hover:opacity-80 transition-colors underline"
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
onClick={(e) => e.stopPropagation()}
>
{children}
</a>
);
};

@ -4,7 +4,7 @@ import { createContext } from "react";
* Context for MemoContent rendering
*
* Provides memo metadata and configuration to child components
* Used by custom react-markdown components (TaskListItem, WikiLink, Tag, etc.)
* Used by custom react-markdown components (TaskListItem, Tag, etc.)
*/
export interface MemoContentContextType {

@ -1,44 +0,0 @@
import React from "react";
/**
* Custom link component for react-markdown wikilinks
*
* Handles [[wikilink]] rendering with custom styling and click behavior.
* The remark-wiki-link plugin converts [[target]] to anchor elements.
*
* Note: This component should only be used for wikilinks.
* Regular links are handled by the default anchor element.
*/
interface WikiLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
node?: any; // AST node from react-markdown
href?: string;
children?: React.ReactNode;
}
export const WikiLink: React.FC<WikiLinkProps> = ({ href, children, ...props }) => {
// Extract target from href
// remark-wiki-link creates hrefs like "#/wiki/target"
const target = href?.replace("#/wiki/", "") || "";
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.stopPropagation();
// TODO: Implement wikilink navigation
// This could navigate to memo detail, show preview, etc.
console.log("Wikilink clicked:", target);
};
return (
<a
{...props}
href={href}
className="wikilink text-primary hover:opacity-80 transition-colors underline"
data-target={target}
onClick={handleClick}
>
{children}
</a>
);
};

@ -3,7 +3,6 @@ import { memo, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import remarkWikiLink from "remark-wiki-link";
import useCurrentUser from "@/hooks/useCurrentUser";
import { cn } from "@/lib/utils";
import { memoStore } from "@/store";
@ -11,12 +10,10 @@ import { useTranslate } from "@/utils/i18n";
import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type";
import { remarkTag } from "@/utils/remark-plugins/remark-tag";
import { isSuperUser } from "@/utils/user";
import { createConditionalComponent, isTagNode, isTaskListItemNode, isWikiLinkNode } from "./ConditionalComponent";
import { DefaultLink } from "./DefaultLink";
import { createConditionalComponent, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
import { MemoContentContext } from "./MemoContentContext";
import { Tag } from "./Tag";
import { TaskListItem } from "./TaskListItem";
import { WikiLink } from "./WikiLink";
// MAX_DISPLAY_HEIGHT is the maximum height of the memo content to display in compact mode.
const MAX_DISPLAY_HEIGHT = 256;
@ -99,12 +96,11 @@ const MemoContent = observer((props: Props) => {
onDoubleClick={onMemoContentDoubleClick}
>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkWikiLink, remarkTag, remarkPreserveType]}
remarkPlugins={[remarkGfm, remarkTag, remarkPreserveType]}
rehypePlugins={[rehypeRaw]}
components={{
// Conditionally render custom components based on AST node type
input: createConditionalComponent(TaskListItem, "input", isTaskListItemNode),
a: createConditionalComponent(WikiLink, DefaultLink, isWikiLinkNode),
span: createConditionalComponent(Tag, "span", isTagNode),
}}
>

@ -22,7 +22,7 @@
.markdown-content ul.contains-task-list,
.prose ul.contains-task-list {
padding: 0 !important;
margin: 0.5rem 0 !important;
margin: 0.25rem 0 !important;
list-style: none !important;
margin-block-start: 0 !important;
margin-block-end: 0 !important;
@ -48,8 +48,8 @@
.prose ul.contains-task-list li.task-list-item {
display: flex !important;
align-items: center !important;
gap: 0.5rem !important;
margin: 0.125rem 0 !important;
gap: 0.375rem !important;
margin: 0.0625rem 0 !important;
padding: 0 !important;
line-height: 1.5rem !important;
list-style: none !important;
@ -105,81 +105,111 @@
/* ========================================
* Markdown Content Styles
* Custom minimal styles for markdown rendering
* Compact spacing optimized for memos/notes
*
* Key principles:
* 1. Block elements use 8px (0.5rem) bottom margin (compact)
* 2. First child has no top margin, last child has no bottom margin
* 3. Nested elements have minimal spacing
* 4. Inline elements have no vertical spacing
* ======================================== */
.markdown-content {
font-size: 1rem;
line-height: 1.5;
color: var(--foreground);
white-space: pre-line; /* Preserve newlines but collapse multiple spaces */
word-wrap: break-word;
}
/* Block elements should not inherit pre-line */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6,
/* ========================================
* First/Last Child Normalization
* Remove boundary spacing to prevent double margins
* ======================================== */
.markdown-content > :first-child {
margin-top: 0 !important;
}
.markdown-content > :last-child {
margin-bottom: 0 !important;
}
/* ========================================
* Block Elements
* Compact 8px bottom margin
* ======================================== */
.markdown-content p,
.markdown-content blockquote,
.markdown-content ul,
.markdown-content ol,
.markdown-content dl,
.markdown-content table,
.markdown-content pre,
.markdown-content blockquote,
.markdown-content table {
white-space: normal;
.markdown-content hr {
margin-top: 0;
margin-bottom: 0.5rem;
}
/* Code blocks need pre-wrap */
.markdown-content pre {
white-space: pre-wrap;
/* ========================================
* Headings
* Compact spacing for visual separation
* ======================================== */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 0.75rem;
margin-bottom: 0.5rem;
font-weight: 600;
line-height: 1.25;
}
/* Headings */
.markdown-content h1 {
font-size: 1.875rem;
font-size: 2em;
font-weight: 700;
line-height: 2.25rem;
margin-top: 1rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25rem;
}
.markdown-content h2 {
font-size: 1.5rem;
font-weight: 600;
line-height: 2rem;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
font-size: 1.5em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25rem;
}
.markdown-content h3 {
font-size: 1.25rem;
font-weight: 600;
line-height: 1.75rem;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
font-size: 1.25em;
}
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
font-size: 1.125rem;
font-weight: 600;
line-height: 1.75rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
.markdown-content h4 {
font-size: 1em;
}
/* First heading has no top margin */
.markdown-content > :first-child {
margin-top: 0;
.markdown-content h5 {
font-size: 0.875em;
}
/* Paragraphs */
.markdown-content h6 {
font-size: 0.85em;
color: var(--muted-foreground);
}
/* ========================================
* Paragraphs
* ======================================== */
.markdown-content p {
margin: 0.5rem 0;
line-height: 1.5;
}
/* Links */
/* ========================================
* Links
* ======================================== */
.markdown-content a {
color: var(--primary);
text-decoration: underline;
@ -190,10 +220,12 @@
opacity: 0.8;
}
/* Lists - MINIMAL spacing */
/* ========================================
* Lists
* ======================================== */
.markdown-content ul,
.markdown-content ol {
margin: 0.5rem 0;
padding-left: 1.5em;
list-style-position: outside;
}
@ -207,25 +239,34 @@
}
.markdown-content li {
margin: 0.125rem 0;
margin-top: 0.125rem;
line-height: 1.5;
}
.markdown-content li > p {
margin-bottom: 0.125rem;
}
/* Nested lists should have minimal spacing */
.markdown-content li > ul,
.markdown-content li > ol {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
/* First and last items in lists */
.markdown-content li:first-child {
margin-top: 0;
}
.markdown-content li:last-child {
margin-bottom: 0;
.markdown-content li + li {
margin-top: 0.125rem;
}
/* Nested lists */
.markdown-content li > ul,
.markdown-content li > ol {
margin: 0.25rem 0;
}
/* ========================================
* Code (inline and blocks)
* ======================================== */
/* Code */
.markdown-content code {
font-family: var(--font-mono);
font-size: 0.875em;
@ -238,76 +279,120 @@
font-family: var(--font-mono);
font-size: 0.875rem;
background: var(--muted);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
overflow-x: auto;
margin: 0.75rem 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.markdown-content pre code {
background: none;
padding: 0;
font-size: inherit;
border-radius: 0;
}
/* Blockquotes */
/* ========================================
* Blockquotes
* ======================================== */
.markdown-content blockquote {
border-left: 3px solid var(--border);
padding-left: 1rem;
margin: 0.75rem 0;
padding: 0 0.75rem;
color: var(--muted-foreground);
font-style: italic;
border-left: 0.25rem solid var(--border);
}
/* Horizontal rule */
.markdown-content blockquote > :first-child {
margin-top: 0;
}
.markdown-content blockquote > :last-child {
margin-bottom: 0;
}
/* ========================================
* Horizontal Rules
* ======================================== */
.markdown-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 1rem 0;
height: 0.25em;
padding: 0;
background: transparent;
border: 0;
border-bottom: 1px solid var(--border);
}
/* Tables */
/* ========================================
* Tables
* ======================================== */
.markdown-content table {
display: block;
width: 100%;
width: max-content;
max-width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
margin: 0.75rem 0;
}
.markdown-content th,
.markdown-content td {
.markdown-content table th,
.markdown-content table td {
padding: 0.25rem 0.5rem;
border: 1px solid var(--border);
padding: 0.5rem;
text-align: left;
}
.markdown-content th {
background: var(--muted);
.markdown-content table th {
font-weight: 600;
background: var(--muted);
}
.markdown-content table tr {
background: transparent;
border-top: 1px solid var(--border);
}
/* Images */
.markdown-content table tr:nth-child(2n) {
background: var(--muted);
opacity: 0.5;
}
/* ========================================
* Images
* ======================================== */
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 0.75rem 0;
}
/* Strong/Bold */
/* ========================================
* Inline Elements
* No vertical spacing
* ======================================== */
.markdown-content strong {
font-weight: 600;
}
/* Emphasis/Italic */
.markdown-content em {
font-style: italic;
}
/* Inline elements shouldn't add vertical spacing */
.markdown-content code,
.markdown-content strong,
.markdown-content em,
.markdown-content a {
vertical-align: baseline;
}
/* ========================================
* Strikethrough (GFM)
* ======================================== */
.markdown-content del {
text-decoration: line-through;
}
}

@ -11,10 +11,11 @@ import { visit } from "unist-util-visit";
* #work <span class="tag" data-tag="work">#work</span>
* #2024_plans <span class="tag" data-tag="2024_plans">#2024_plans</span>
* #work-notes <span class="tag" data-tag="work-notes">#work-notes</span>
* #tag1/subtag/subtag2 <span class="tag" data-tag="tag1/subtag/subtag2">#tag1/subtag/subtag2</span>
*
* Rules:
* - Tag must start with # followed by alphanumeric, underscore, or hyphen
* - Tag ends at whitespace, punctuation (except - and _), or end of line
* - Tag must start with # followed by alphanumeric, underscore, hyphen, or forward slash
* - Tag ends at whitespace, punctuation (except -, _, /), or end of line
* - Tags at start of line after ## are headings, not tags
*/
@ -22,7 +23,7 @@ import { visit } from "unist-util-visit";
* Check if character is valid for tag content
*/
function isTagChar(char: string): boolean {
return /[a-zA-Z0-9_-]/.test(char);
return /[a-zA-Z0-9_\-/]/.test(char);
}
/**

Loading…
Cancel
Save