mirror of https://github.com/usememos/memos
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
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue