diff --git a/internal/block/block.go b/internal/block/block.go index 5c6ad71..fad6d27 100644 --- a/internal/block/block.go +++ b/internal/block/block.go @@ -27,7 +27,7 @@ const ( Callout // > [!NOTE] callout/admonition Table // GFM pipe table Kanban // ```kanban``` fenced kanban board - Bookmark // [title](url) link card + Link // [title](url) link card ) // String returns the human-readable name of a BlockType. @@ -63,8 +63,8 @@ func (bt BlockType) String() string { return "Table" case Kanban: return "Kanban" - case Bookmark: - return "Bookmark" + case Link: + return "Link" default: return "Unknown" } @@ -113,8 +113,8 @@ func (bt BlockType) Short() string { return "tb" case Kanban: return "kb" - case Bookmark: - return "bm" + case Link: + return "ln" default: return "?" } @@ -278,10 +278,10 @@ func ExtractDefinition(content string) (term, definition string) { return first, "" } -// ExtractBookmark splits a bookmark block's content into its title line +// ExtractLink splits a link block's content into its title line // (first line) and URL (second line). When only a URL is stored, title // is empty. -func ExtractBookmark(content string) (title, url string) { +func ExtractLink(content string) (title, url string) { first, rest, found := strings.Cut(content, "\n") if found { return first, rest diff --git a/internal/block/parse.go b/internal/block/parse.go index db15494..6edb3d8 100644 --- a/internal/block/parse.go +++ b/internal/block/parse.go @@ -6,18 +6,18 @@ import ( ) var ( - bookmarkLinkRe = regexp.MustCompile(`^\[([^\]]*)\]\((https?://[^\s)]+)\)\s*$`) - bookmarkBareRe = regexp.MustCompile(`^(https?://\S+)\s*$`) + linkLinkRe = regexp.MustCompile(`^\[([^\]]*)\]\((https?://[^\s)]+)\)\s*$`) + linkBareRe = regexp.MustCompile(`^(https?://\S+)\s*$`) ) -// ParseBookmark reports whether a single line is a bookmark — either a +// ParseLink reports whether a single line is a link — either a // titled markdown link `[title](url)` or a bare http(s) URL — and returns // the extracted title (may be empty) and URL. -func ParseBookmark(line string) (title, url string, ok bool) { - if m := bookmarkLinkRe.FindStringSubmatch(line); m != nil { +func ParseLink(line string) (title, url string, ok bool) { + if m := linkLinkRe.FindStringSubmatch(line); m != nil { return m[1], m[2], true } - if m := bookmarkBareRe.FindStringSubmatch(line); m != nil { + if m := linkBareRe.FindStringSubmatch(line); m != nil { return "", m[1], true } return "", "", false @@ -221,13 +221,13 @@ func Parse(markdown string) []Block { continue } - // --- Bookmark ([title](url) or bare URL on its own line) --- - if title, url, ok := ParseBookmark(line); ok { + // --- Link ([title](url) or bare URL on its own line) --- + if title, url, ok := ParseLink(line); ok { content := url if title != "" { content = title + "\n" + url } - blocks = append(blocks, Block{Type: Bookmark, Content: content}) + blocks = append(blocks, Block{Type: Link, Content: content}) i++ continue } @@ -300,7 +300,7 @@ func isBlockStart(line string) bool { if strings.HasPrefix(line, "![[") && strings.HasSuffix(line, "]]") { return true } - if _, _, ok := ParseBookmark(line); ok { + if _, _, ok := ParseLink(line); ok { return true } return isDivider(line) diff --git a/internal/block/parse_test.go b/internal/block/parse_test.go index e1e514c..b222ecf 100644 --- a/internal/block/parse_test.go +++ b/internal/block/parse_test.go @@ -313,46 +313,46 @@ func TestParse(t *testing.T) { }, }, { - name: "bookmark titled link", + name: "link titled link", input: "[Example](https://example.com)", expect: []Block{ - {Type: Bookmark, Content: "Example\nhttps://example.com"}, + {Type: Link, Content: "Example\nhttps://example.com"}, }, }, { - name: "bookmark bare url", + name: "link bare url", input: "https://example.com", expect: []Block{ - {Type: Bookmark, Content: "https://example.com"}, + {Type: Link, Content: "https://example.com"}, }, }, { - name: "bookmark http url", + name: "link http url", input: "http://example.com/path?q=1", expect: []Block{ - {Type: Bookmark, Content: "http://example.com/path?q=1"}, + {Type: Link, Content: "http://example.com/path?q=1"}, }, }, { - name: "bookmark between paragraphs", + name: "link between paragraphs", input: "above\n\n[Site](https://site.io)\n\nbelow", expect: []Block{ {Type: Paragraph, Content: "above"}, {Type: Paragraph, Content: ""}, - {Type: Bookmark, Content: "Site\nhttps://site.io"}, + {Type: Link, Content: "Site\nhttps://site.io"}, {Type: Paragraph, Content: ""}, {Type: Paragraph, Content: "below"}, }, }, { - name: "bookmark titled link with empty title", + name: "link titled link with empty title", input: "[](https://example.com)", expect: []Block{ - {Type: Bookmark, Content: "https://example.com"}, + {Type: Link, Content: "https://example.com"}, }, }, { - name: "inline link in paragraph not parsed as bookmark", + name: "inline link in paragraph not parsed as link", input: "see [docs](https://example.com) for details", expect: []Block{ {Type: Paragraph, Content: "see [docs](https://example.com) for details"}, @@ -417,8 +417,8 @@ func formatBlocks(blocks []Block) string { b.WriteString("Embed") case Table: b.WriteString("Table") - case Bookmark: - b.WriteString("Bookmark") + case Link: + b.WriteString("Link") } if bl.Content != "" { b.WriteString(" " + bl.Content) diff --git a/internal/block/serialize.go b/internal/block/serialize.go index 965eb99..7e795d1 100644 --- a/internal/block/serialize.go +++ b/internal/block/serialize.go @@ -103,8 +103,8 @@ func Serialize(blocks []Block) string { case Embed: lines = append(lines, "![["+b.Content+"]]") - case Bookmark: - title, url := ExtractBookmark(b.Content) + case Link: + title, url := ExtractLink(b.Content) if title == "" { lines = append(lines, url) } else { diff --git a/internal/block/serialize_test.go b/internal/block/serialize_test.go index 7e45d0d..bc1706f 100644 --- a/internal/block/serialize_test.go +++ b/internal/block/serialize_test.go @@ -166,15 +166,15 @@ func TestSerializeRoundTrip(t *testing.T) { md: "| Name | Age |\n| ---- | --- |", }, { - name: "bookmark titled link", + name: "link titled link", md: "[Example](https://example.com)", }, { - name: "bookmark bare url", + name: "link bare url", md: "https://example.com", }, { - name: "bookmark between paragraphs", + name: "link between paragraphs", md: "above\n\n[Site](https://site.io)\n\nbelow", }, } diff --git a/internal/editor/editor.go b/internal/editor/editor.go index 038f548..7ec87eb 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -749,6 +749,11 @@ func (m *Model) navigateUp() { } ta := &m.textareas[m.active] + if m.blocks[m.active].Type == block.Link { + ta.MoveToBegin() + ta.CursorEnd() + return + } ta.MoveToEnd() li := ta.LineInfo() ta.SetCursorColumn(li.StartColumn + charOffset) @@ -777,13 +782,18 @@ func (m *Model) navigateDown() { } ta := &m.textareas[m.active] + if m.blocks[m.active].Type == block.Link { + ta.MoveToBegin() + ta.CursorEnd() + return + } ta.MoveToBegin() ta.SetCursorColumn(charOffset) } // isMultiLine returns true if the block type allows multi-line content. func isMultiLine(bt block.BlockType) bool { - return bt == block.Paragraph || bt == block.CodeBlock || bt == block.Quote || bt == block.DefinitionList || bt == block.Callout || bt == block.Table || bt == block.Bookmark + return bt == block.Paragraph || bt == block.CodeBlock || bt == block.Quote || bt == block.DefinitionList || bt == block.Callout || bt == block.Table || bt == block.Link } // insertBlockBefore inserts a new block before the given index, creates a @@ -1169,12 +1179,12 @@ func (m *Model) handleEnter() { } } - // Bookmark: two-line internal structure (title on line 0, URL on line 1). + // Link: two-line internal structure (title on line 0, URL on line 1). // Enter on the title line moves the cursor to the URL line. // Enter on the URL line exits the block to a new paragraph below. - if bt == block.Bookmark { + if bt == block.Link { trimmed := strings.TrimSpace(strings.ReplaceAll(content, "\n", "")) - if trimmed == "" || trimmed == "https://" || trimmed == "http://" { + if trimmed == "" { m.blocks[m.active].Type = block.Paragraph m.blocks[m.active].Content = "" newTA := newTextareaForBlock(m.blocks[m.active], m.width) @@ -1182,6 +1192,12 @@ func (m *Model) handleEnter() { m.textareas[m.active] = newTA return } + // Enter at the very start of the title pushes the link down by + // inserting a new paragraph above. + if ta.Line() == 0 && ta.LineInfo().ColumnOffset == 0 { + m.insertBlockBefore(m.active, block.Block{Type: block.Paragraph}) + return + } if ta.Line() == 0 { lines := strings.Split(content, "\n") if len(lines) < 2 { @@ -1406,6 +1422,12 @@ func (m *Model) handleBackspace() bool { case block.DefinitionList: // Keep only the term line. keepContent, _ = block.ExtractDefinition(content) + case block.Link: + title, url := block.ExtractLink(content) + keepContent = title + if keepContent == "" { + keepContent = url + } } m.convertToParagraph(keepContent) return true @@ -1558,10 +1580,8 @@ func (m *Model) applyPaletteSelection(bt block.BlockType) { m.textareas[m.active].MoveToBegin() m.cursorCmd = m.textareas[m.active].Focus() } - // Bookmark: seed with "Title\nhttps://" so the user types the title - // first then arrows down to fill in the URL. - if bt == block.Bookmark && !strings.Contains(m.textareas[m.active].Value(), "\n") { - m.textareas[m.active].SetValue("\nhttps://") + if bt == block.Link && !strings.Contains(m.textareas[m.active].Value(), "\n") { + m.textareas[m.active].SetValue("\n") m.textareas[m.active].MoveToBegin() m.cursorCmd = m.textareas[m.active].Focus() } @@ -1837,6 +1857,20 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { m.openEmbedModal(idx) return m, nil } + if m.blocks[idx].Type == block.Link { + _, u := block.ExtractLink(m.blocks[idx].Content) + if u != "" { + if err := openURL(u); err == nil { + m.status = "Opened: " + u + m.statusStyle = statusSuccess + } else { + m.status = "Open failed: " + err.Error() + m.statusStyle = statusError + } + return m, m.scheduleStatusDismiss() + } + return m, nil + } } } else { m.hoverBlock = -1 @@ -2107,7 +2141,21 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.updateViewport() return m, nil - case "esc", "ctrl+c": + case "esc": + m.viewMode = false + m.hoverBlock = -1 + if m.active >= 0 && m.active < len(m.textareas) { + m.cursorCmd = m.textareas[m.active].Focus() + if m.blocks[m.active].Type == block.Table { + m.table = initTable(m.blocks[m.active].Content) + cw := m.tableCellTAWidth() + m.table.loadCell(&m.textareas[m.active], cw, false) + m.cursorCmd = m.textareas[m.active].Focus() + } + } + m.updateViewport() + return m, nil + case "ctrl+c": if m.modified() { m.quitPrompt = true m.status = "Save before quitting? [Y/n/Esc]" @@ -2263,6 +2311,19 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case "up": + // Link blocks: up always navigates to the previous block, + // never swaps between title and URL slots. + if m.blocks[m.active].Type == block.Link { + if m.active == 0 { + m.pushUndo() + m.insertBlockBefore(0, block.Block{Type: block.Paragraph}) + m.updateViewport() + return m, nil + } + m.navigateUp() + m.updateViewport() + return m, nil + } // Table: move to cell above, preserving horizontal position. if m.isAtFirstLine() && m.table != nil && m.table.row > 0 { ta := &m.textareas[m.active] @@ -2290,6 +2351,15 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "down": + // Link blocks: down always navigates to the next block, + // never swaps between title and URL slots. + if m.blocks[m.active].Type == block.Link { + if m.active < len(m.textareas)-1 { + m.navigateDown() + m.updateViewport() + } + return m, nil + } // Table: move to cell below, preserving horizontal position. if m.isAtLastLine() && m.table != nil && m.table.row < len(m.table.cells)-1 { ta := &m.textareas[m.active] @@ -2429,6 +2499,25 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { m.defLookup.open(m.blocks, m.remoteDefinitions()) m.updateViewport() return m, nil + case block.Link: + content := m.blocks[m.active].Content + if m.active < len(m.textareas) { + content = m.textareas[m.active].Value() + } + _, u := block.ExtractLink(content) + if u == "" { + m.status = "No URL to open" + m.statusStyle = statusWarning + return m, m.scheduleStatusDismiss() + } + if err := openURL(u); err != nil { + m.status = "Open failed: " + err.Error() + m.statusStyle = statusError + } else { + m.status = "Opened: " + u + m.statusStyle = statusSuccess + } + return m, m.scheduleStatusDismiss() } } @@ -2500,6 +2589,26 @@ func (m Model) update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + // On Link blocks, Tab/Shift+Tab jumps between title and URL slots. + if keyMsg.Code == tea.KeyTab && m.blocks[m.active].Type == block.Link { + ta := &m.textareas[m.active] + content := ta.Value() + if !strings.Contains(content, "\n") { + ta.SetValue(content + "\n") + } + if keyMsg.Mod.Contains(tea.ModShift) { + ta.MoveToBegin() + ta.CursorEnd() + } else { + ta.MoveToBegin() + ta.CursorDown() + ta.CursorEnd() + } + m.cursorCmd = ta.Focus() + m.updateViewport() + return m, nil + } + // On Embed blocks, Tab opens the note picker. if keyMsg.Code == tea.KeyTab && m.blocks[m.active].Type == block.Embed { if m.config.ListEmbedTargets != nil { @@ -3452,6 +3561,8 @@ func (m Model) blockHint() string { return "\u2190\u2192 col \u00b7 \u2191\u2193 card \u00b7 \u21e7+arrows move \u00b7 n new \u00b7 \u23ce edit \u00b7 p prio \u00b7 s sort \u00b7 bksp del (board if col empty)" case block.Embed: return "\u2303X open \u00B7 Tab pick" + case block.Link: + return "\u2303X open \u00B7 Tab url" case block.DefinitionList: return "\u2303X search" default: diff --git a/internal/editor/openurl.go b/internal/editor/openurl.go new file mode 100644 index 0000000..493f696 --- /dev/null +++ b/internal/editor/openurl.go @@ -0,0 +1,22 @@ +package editor + +import ( + "fmt" + "os/exec" + "runtime" +) + +func openURL(url string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + default: + return fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } + return cmd.Start() +} diff --git a/internal/editor/palette.go b/internal/editor/palette.go index 0a8413b..94dff48 100644 --- a/internal/editor/palette.go +++ b/internal/editor/palette.go @@ -85,7 +85,7 @@ var paletteItemDefs = []struct { {"!", "Callout", block.Callout}, {"\u2014", "Divider", block.Divider}, {"\u2197", "Embed", block.Embed}, - {"\u29c9", "Bookmark", block.Bookmark}, + {"\u29c9", "Link", block.Link}, } // defaultPaletteItems returns the full list of block-type entries. diff --git a/internal/editor/render.go b/internal/editor/render.go index 3add848..83df2b8 100644 --- a/internal/editor/render.go +++ b/internal/editor/render.go @@ -3,6 +3,7 @@ package editor import ( "bytes" "fmt" + neturl "net/url" "strings" "github.com/alecthomas/chroma/v2" @@ -425,12 +426,7 @@ func (m Model) renderActiveBlock(idx int, b block.Block, _ string) string { rendered = prefixFirstLine(prefix, taView) } - case block.Bookmark: - bs := th.Blocks.Bookmark - titleColor := resolveColor(bs.TitleColor, th.Accent) - urlColor := resolveColor(bs.URLColor, th.Muted) - titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(titleColor)) - urlStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(urlColor)).Underline(true) + case block.Link: rawLines := strings.Split(ta.Value(), "\n") title := "" url := "" @@ -441,22 +437,11 @@ func (m Model) renderActiveBlock(idx int, b block.Block, _ string) string { url = rawLines[1] } cursorOnTitle := ta.Line() == 0 - var titleLine, urlLine string - if title == "" { - titleLine = renderPlaceholder("Bookmark title", cursorOnTitle) - } else if cursorOnTitle { - titleLine = renderLabelCursor(title, ta.LineInfo().ColumnOffset, titleStyle) - } else { - titleLine = titleStyle.Render(title) - } - if url == "" { - urlLine = renderPlaceholder("https://", !cursorOnTitle) - } else if !cursorOnTitle { - urlLine = renderLabelCursor(url, ta.LineInfo().ColumnOffset, urlStyle) - } else { - urlLine = urlStyle.Render(url) - } - rendered = titleLine + "\n" + urlLine + col := ta.LineInfo().ColumnOffset + chip, vCol := renderLinkChipActive(title, url, cursorOnTitle, col) + rendered = chip + cursorVisIdx = 0 + cursorColInWrap = vCol - blockPrefixWidth(b) case block.Callout: cs := th.Blocks.Callout @@ -505,7 +490,12 @@ func (m Model) renderActiveBlock(idx int, b block.Block, _ string) string { } // Truncate or horizontally scroll lines that exceed terminal width. - if m.wordWrap { + if b.Type == block.Link { + cursorCol := gutterWidth + blockPrefixWidth(b) + cursorColInWrap + for i, l := range lines { + lines[i] = scrollOrTruncate(l, m.width, cursorCol, i == cursorVisIdx) + } + } else if m.wordWrap { for i, l := range lines { if lipgloss.Width(l) > m.width { lines[i] = ansi.Truncate(l, m.width, "") @@ -535,61 +525,136 @@ func (m Model) renderActiveBlock(idx int, b block.Block, _ string) string { return strings.Join(lines, "\n") } -// renderBookmarkCard renders a bookmark as a bordered card with title on -// top and URL below. width is the content column width. -func renderBookmarkCard(content string, width int, hovered bool) string { +func linkStyles() (icon, title, host lipgloss.Style) { th := theme.Current() - bs := th.Blocks.Bookmark - borderColor := resolveColor(bs.Border, th.Border) + bs := th.Blocks.Link titleColor := resolveColor(bs.TitleColor, th.Accent) urlColor := resolveColor(bs.URLColor, th.Muted) + icon = lipgloss.NewStyle().Foreground(lipgloss.Color(titleColor)) + title = lipgloss.NewStyle().Foreground(lipgloss.Color(titleColor)) + host = lipgloss.NewStyle().Foreground(lipgloss.Color(urlColor)).Faint(true) + return +} + +const linkIcon = "↗ " +const linkSep = " " + +func renderLinkCard(content string, width int, hovered bool) string { + iconStyle, titleStyle, hostStyle := linkStyles() + if hovered { + titleStyle = titleStyle.Underline(true) + } + + title, url := block.ExtractLink(content) - title, url := block.ExtractBookmark(content) if title == "" && url == "" { - title = "Bookmark" + return iconStyle.Render(linkIcon) + renderPlaceholder("Link title", false) } - if title == "" { - title = url + + display := title + showHost := true + if display == "" { + display = url + showHost = false } - innerW := width - 4 - if innerW < 10 { - innerW = 10 + host := "" + if showHost && url != "" { + host = linkHost(url) } - titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(titleColor)) - urlStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(urlColor)) - if hovered { - urlStyle = urlStyle.Underline(true) - titleStyle = titleStyle.Underline(true) + + iconW := lipgloss.Width(linkIcon) + sepW := lipgloss.Width(linkSep) + hostW := lipgloss.Width(host) + + if width > 0 { + avail := width - iconW + if host != "" { + avail -= sepW + hostW + } + if avail < 4 { + avail = 4 + host = "" + } + if lipgloss.Width(display) > avail { + display = ansi.Truncate(display, avail, "…") + } } - bc := lipgloss.NewStyle().Foreground(lipgloss.Color(borderColor)) - titleR := []rune(title) - if len(titleR) > innerW { - titleR = append(titleR[:innerW-1], '…') + titleRendered := titleStyle.Render(display) + if url != "" { + titleRendered = osc8Wrap(url, titleRendered) } - urlR := []rune(url) - if len(urlR) > innerW { - urlR = append(urlR[:innerW-1], '…') + + out := iconStyle.Render(linkIcon) + titleRendered + if host != "" { + out += linkSep + hostStyle.Render(host) } - titleLine := titleStyle.Render(string(titleR)) - urlLine := urlStyle.Render(string(urlR)) + return out +} - titlePad := innerW - len([]rune(string(titleR))) - if titlePad < 0 { - titlePad = 0 +// renderLinkChipActive renders the link chip while editing. It returns the +// chip text and the visual cursor column (relative to the start of the +// rendered chip, including the icon) so the surrounding scroll math can +// keep the cursor in view. +func renderLinkChipActive(title, url string, cursorOnTitle bool, cursorCol int) (string, int) { + iconStyle, titleStyle, hostStyle := linkStyles() + urlEditStyle := hostStyle.Underline(false) + + var titleSlot string + var titleSlotPlain string + if title == "" { + titleSlot = renderPlaceholder("Link title", cursorOnTitle) + titleSlotPlain = "Link title" + } else if cursorOnTitle { + titleSlot = renderLabelCursor(title, cursorCol, titleStyle) + titleSlotPlain = title + } else { + titleSlot = titleStyle.Render(title) + titleSlotPlain = title } - urlPad := innerW - len([]rune(string(urlR))) - if urlPad < 0 { - urlPad = 0 + + var urlSlot string + if url == "" { + urlSlot = renderPlaceholder("https://", !cursorOnTitle) + } else if !cursorOnTitle { + urlSlot = renderLabelCursor(url, cursorCol, urlEditStyle) + } else { + host := linkHost(url) + if host == "" { + host = url + } + urlSlot = hostStyle.Render(host) } - top := bc.Render("╭" + strings.Repeat("─", innerW+2) + "╮") - bottom := bc.Render("╰" + strings.Repeat("─", innerW+2) + "╯") - bar := bc.Render("│") - titleRow := bar + " " + titleLine + strings.Repeat(" ", titlePad) + " " + bar - urlRow := bar + " " + urlLine + strings.Repeat(" ", urlPad) + " " + bar - return strings.Join([]string{top, titleRow, urlRow, bottom}, "\n") + chip := iconStyle.Render(linkIcon) + titleSlot + linkSep + urlSlot + + iconW := lipgloss.Width(linkIcon) + titleW := lipgloss.Width(titleSlotPlain) + if title == "" { + titleW = lipgloss.Width("Link title") + } + sepW := lipgloss.Width(linkSep) + + var vCol int + if cursorOnTitle { + vCol = iconW + cursorCol + } else { + vCol = iconW + titleW + sepW + cursorCol + } + return chip, vCol +} + +func linkHost(raw string) string { + u, err := neturl.Parse(raw) + if err != nil || u.Host == "" { + return raw + } + return u.Host +} + +func osc8Wrap(url, text string) string { + return "\x1b]8;;" + url + "\x1b\\" + text + "\x1b]8;;\x1b\\" } // renderCodeBox renders code in a bordered box with the label always in the @@ -986,8 +1051,8 @@ func renderInactiveBlock(b block.Block, content string, width int, wordWrap bool Render(icon + wrapped) } - case block.Bookmark: - rendered = renderBookmarkCard(content, contentWidth, false) + case block.Link: + rendered = renderLinkCard(content, contentWidth, false) case block.Table: tableWidth := width - gutterWidth @@ -1213,8 +1278,8 @@ func renderViewBlock(b block.Block, content string, width int, wordWrap bool, bl rendered = style.Render(icon + wrapped) } - case block.Bookmark: - rendered = renderBookmarkCard(content, contentWidth, hovered) + case block.Link: + rendered = renderLinkCard(content, contentWidth, hovered) case block.Table: rendered = renderTableGrid(content, contentWidth, th.Border, th.Blocks.Table.HeaderBold, true) diff --git a/internal/theme/theme.go b/internal/theme/theme.go index 75987e5..24358ee 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -118,8 +118,8 @@ type TableStyle struct { HeaderBold bool // whether header row is rendered bold } -// BookmarkStyle controls bookmark card rendering. -type BookmarkStyle struct { +// LinkStyle controls link card rendering. +type LinkStyle struct { Border string // hex border color; "" means theme.Border TitleColor string // hex title color; "" means theme.Accent URLColor string // hex url color; "" means theme.Muted @@ -140,7 +140,7 @@ type BlockStyles struct { Definition DefinitionStyle Embed EmbedStyle Table TableStyle - Bookmark BookmarkStyle + Link LinkStyle } // DefaultBlockStyles returns the baseline block styles that match the original