Browse Source

citations continued

make new post responsive
imwald
Silberengel 3 months ago
parent
commit
d944a31c96
  1. 465
      src/components/CitationCard/index.tsx
  2. 2
      src/components/EmbeddedCitation/index.tsx
  3. 117
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  4. 35
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 33
      src/components/PostEditor/PostContent.tsx

465
src/components/CitationCard/index.tsx

@ -2,8 +2,8 @@ import { ExtendedKind } from '@/constants' @@ -2,8 +2,8 @@ import { ExtendedKind } from '@/constants'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ExternalLink, Book, FileText, Bot } from 'lucide-react'
import { ExternalLink } from 'lucide-react'
import { nip19 } from 'nostr-tools'
function getTagValue(event: Event, tagName: string): string {
return event.tags.find(tag => tag[0] === tagName)?.[1] || ''
@ -13,9 +13,10 @@ interface CitationCardProps { @@ -13,9 +13,10 @@ interface CitationCardProps {
event: Event
className?: string
displayType?: 'end' | 'foot' | 'foot-end' | 'inline' | 'quote' | 'prompt-end' | 'prompt-inline'
citationId?: string // The original citation ID (nevent/naddr) for Nostr references
}
export default function CitationCard({ event, className, displayType = 'end' }: CitationCardProps) {
export default function CitationCard({ event, className, displayType = 'end', citationId }: CitationCardProps) {
const { t } = useTranslation()
const citationData = useMemo(() => {
@ -122,29 +123,252 @@ export default function CitationCard({ event, className, displayType = 'end' }: @@ -122,29 +123,252 @@ export default function CitationCard({ event, className, displayType = 'end' }:
}
}
const formatYear = (dateStr: string) => {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
if (!isNaN(date.getTime())) {
return date.getFullYear().toString()
}
} catch {
// Fall through to regex extraction
}
// Try to extract year from string (YYYY format)
const yearMatch = dateStr.match(/\b(19|20)\d{2}\b/)
return yearMatch ? yearMatch[0] : ''
}
// Format citation in academic style (NKBIP-03 format for Nostr references)
const formatAcademicCitation = () => {
if (citationData.type === 'internal') {
// NKBIP-03 format: [author]. Nostr: "[title]". [published on].\nnostr:[npub]\nnostr:[event identifier]
const parts: string[] = []
// Author
if (citationData.author) {
parts.push(citationData.author + '.')
}
// Nostr: "[title]"
if (citationData.title) {
parts.push(`Nostr: "${citationData.title}".`)
} else if (citationData.summary) {
// Use summary if no title
const summaryText = citationData.summary.length > 100
? citationData.summary.substring(0, 100) + '...'
: citationData.summary
parts.push(`Nostr: "${summaryText}".`)
} else {
parts.push('Nostr:')
}
// Published on date
if (citationData.publishedOn) {
const dateStr = formatDate(citationData.publishedOn)
if (dateStr) {
parts.push(dateStr + '.')
}
}
// Nostr addresses on separate lines (NKBIP-03 format)
const nostrLines: string[] = []
// Extract npub from cTag (format: kind:pubkey:hex)
if (citationData.cTag) {
const cTagParts = citationData.cTag.split(':')
if (cTagParts.length >= 2) {
const pubkeyHex = cTagParts[1]
// Convert hex pubkey to npub
if (pubkeyHex && /^[0-9a-f]{64}$/i.test(pubkeyHex)) {
try {
const npub = nip19.npubEncode(pubkeyHex)
nostrLines.push(`nostr:${npub}`)
} catch (error) {
// If encoding fails, skip npub line
}
} else if (pubkeyHex.startsWith('npub') || pubkeyHex.startsWith('nprofile')) {
// Already a bech32 address
nostrLines.push(`nostr:${pubkeyHex}`)
}
} else if (citationData.cTag.startsWith('npub') || citationData.cTag.startsWith('nprofile')) {
// cTag is already a bech32 address
nostrLines.push(`nostr:${citationData.cTag}`)
}
}
// Add citationId (event identifier) if it's a bech32 address (nevent/naddr)
if (citationId && (citationId.startsWith('nevent') || citationId.startsWith('naddr') || citationId.startsWith('note'))) {
nostrLines.push(`nostr:${citationId}`)
}
// Join main parts and add nostr addresses on new lines
const mainText = parts.join(' ')
if (nostrLines.length > 0) {
return mainText + '\n' + nostrLines.join('\n')
}
return mainText
} else if (citationData.type === 'external') {
// APA format: Author. (Year). Title. Publisher. URL
const parts: string[] = []
if (citationData.author) {
parts.push(citationData.author)
}
const year = formatYear(citationData.publishedOn || '')
if (year) {
parts.push(`(${year})`)
}
if (citationData.title) {
parts.push(citationData.title + '.')
}
if (citationData.publishedBy) {
parts.push(citationData.publishedBy + '.')
}
if (citationData.url) {
parts.push(citationData.url)
}
const accessedYear = formatYear(citationData.accessedOn)
if (accessedYear && accessedYear !== year) {
parts.push(`Retrieved ${formatDate(citationData.accessedOn)}`)
}
return parts.join(' ')
} else if (citationData.type === 'hardcopy') {
// APA format: Author. (Year). Title. In Editor (Ed.), Published In (Vol. X, pp. Y-Z). Publisher.
const parts: string[] = []
if (citationData.author) {
parts.push(citationData.author)
}
const year = formatYear(citationData.publishedOn || '')
if (year) {
parts.push(`(${year})`)
}
if (citationData.chapterTitle) {
parts.push(citationData.chapterTitle + '.')
if (citationData.editor) {
parts.push(`In ${citationData.editor} (Ed.),`)
}
} else if (citationData.title) {
parts.push(citationData.title + '.')
}
if (citationData.publishedIn) {
const publishedInText = citationData.volume
? `${citationData.publishedIn} (Vol. ${citationData.volume})`
: citationData.publishedIn
parts.push(publishedInText + '.')
}
if (citationData.pageRange) {
parts.push(`pp. ${citationData.pageRange}.`)
}
if (citationData.publishedBy) {
parts.push(citationData.publishedBy + '.')
}
if (citationData.doi) {
parts.push(`https://doi.org/${citationData.doi}`)
}
return parts.join(' ')
} else if (citationData.type === 'prompt') {
// APA format for AI: LLM. (Year). [Prompt interaction]. URL
const parts: string[] = []
if (citationData.llm) {
parts.push(citationData.llm)
}
const year = formatYear(citationData.accessedOn)
if (year) {
parts.push(`(${year})`)
}
parts.push('[Prompt interaction].')
if (citationData.url) {
parts.push(citationData.url)
}
return parts.join(' ')
}
return ''
}
const renderCitationContent = () => {
if (citationData.type === 'internal') {
// NKBIP-03 format: [author]. Nostr: "[title]". [published on].\nnostr:[npub]\nnostr:[event identifier]
const nostrAddresses: string[] = []
// Extract npub from cTag (format: kind:pubkey:hex)
if (citationData.cTag) {
const cTagParts = citationData.cTag.split(':')
if (cTagParts.length >= 2) {
const pubkeyHex = cTagParts[1]
// Convert hex pubkey to npub
if (pubkeyHex && /^[0-9a-f]{64}$/i.test(pubkeyHex)) {
try {
const npub = nip19.npubEncode(pubkeyHex)
nostrAddresses.push(`nostr:${npub}`)
} catch (error) {
// If encoding fails, skip npub line
}
} else if (pubkeyHex.startsWith('npub') || pubkeyHex.startsWith('nprofile')) {
// Already a bech32 address
nostrAddresses.push(`nostr:${pubkeyHex}`)
}
} else if (citationData.cTag.startsWith('npub') || citationData.cTag.startsWith('nprofile')) {
// cTag is already a bech32 address
nostrAddresses.push(`nostr:${citationData.cTag}`)
}
}
// Add citationId (event identifier) if it's a bech32 address (nevent/naddr)
if (citationId && (citationId.startsWith('nevent') || citationId.startsWith('naddr') || citationId.startsWith('note'))) {
nostrAddresses.push(`nostr:${citationId}`)
}
return (
<div className="space-y-1 text-sm">
{citationData.author && (
<div className="font-semibold">{citationData.author}</div>
{/* Main citation line: Author. Nostr: "Title". Published on. */}
<div>
{citationData.author && <span className="font-semibold">{citationData.author}</span>}
{citationData.author && '. '}
<span>Nostr: </span>
{citationData.title ? (
<span className="italic">"{citationData.title}"</span>
) : citationData.summary ? (
<span className="italic">"{citationData.summary.length > 100 ? citationData.summary.substring(0, 100) + '...' : citationData.summary}"</span>
) : null}
{citationData.title || citationData.summary ? '. ' : ''}
{citationData.publishedOn && (
<span className="text-muted-foreground">{formatDate(citationData.publishedOn)}</span>
)}
{citationData.title && (
<div className="italic">"{citationData.title}"</div>
{citationData.publishedOn && '.'}
</div>
{/* Nostr addresses on separate lines */}
{nostrAddresses.map((addr, idx) => (
<div key={idx} className="text-xs text-muted-foreground font-mono break-all mt-1">
{addr}
</div>
))}
{/* ALL additional fields */}
{citationData.accessedOn && (
<div className="text-xs text-muted-foreground">
{t('Accessed on')} {formatDate(citationData.accessedOn)}
</div>
)}
{citationData.publishedOn && (
<div className="text-muted-foreground">{formatDate(citationData.publishedOn)}</div>
{citationData.location && (
<div className="text-xs text-muted-foreground">
{t('Location')}: {citationData.location}
</div>
)}
{citationData.cTag && (
<div className="text-xs text-muted-foreground font-mono break-all">
nostr:{citationData.cTag}
{citationData.geohash && (
<div className="text-xs text-muted-foreground font-mono">
{t('Geohash')}: {citationData.geohash}
</div>
)}
{citationData.summary && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div>
{citationData.relayHint && (
<div className="text-xs text-muted-foreground">
{t('Relay')}: {citationData.relayHint}
</div>
)}
{citationData.summary && citationData.title && (
<div className="text-muted-foreground mt-2 text-xs whitespace-pre-wrap">{citationData.summary}</div>
)}
{event.content && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary">
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary whitespace-pre-wrap">
{event.content}
</div>
)}
@ -181,62 +405,109 @@ export default function CitationCard({ event, className, displayType = 'end' }: @@ -181,62 +405,109 @@ export default function CitationCard({ event, className, displayType = 'end' }:
{citationData.version && (
<div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div>
)}
{citationData.location && (
<div className="text-xs text-muted-foreground">
{t('Location')}: {citationData.location}
</div>
)}
{citationData.geohash && (
<div className="text-xs text-muted-foreground font-mono">
{t('Geohash')}: {citationData.geohash}
</div>
)}
{citationData.openTimestamp && (
<div className="text-xs text-muted-foreground font-mono">
{t('Open Timestamp')}: {citationData.openTimestamp}
</div>
)}
{citationData.summary && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div>
<div className="text-muted-foreground mt-2 whitespace-pre-wrap">{citationData.summary}</div>
)}
{event.content && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary">
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary whitespace-pre-wrap">
{event.content}
</div>
)}
</div>
)
} else if (citationData.type === 'hardcopy') {
// Display ALL hardcopy fields - show everything that exists
// Debug: Log all fields to see what we have
console.log('Hardcopy citation data:', {
author: citationData.author,
title: citationData.title,
chapterTitle: citationData.chapterTitle,
editor: citationData.editor,
publishedIn: citationData.publishedIn,
volume: citationData.volume,
publishedBy: citationData.publishedBy,
publishedOn: citationData.publishedOn,
pageRange: citationData.pageRange,
doi: citationData.doi,
accessedOn: citationData.accessedOn,
version: citationData.version,
location: citationData.location,
geohash: citationData.geohash,
summary: citationData.summary,
content: event.content,
allTags: event.tags
})
return (
<div className="space-y-1 text-sm">
{citationData.author && (
{citationData.author && citationData.author.trim() !== '' && (
<div className="font-semibold">{citationData.author}</div>
)}
{citationData.title && (
{citationData.title && citationData.title.trim() !== '' && (
<div className="italic">"{citationData.title}"</div>
)}
{citationData.chapterTitle && (
{citationData.chapterTitle && citationData.chapterTitle.trim() !== '' && (
<div className="text-muted-foreground">{t('Chapter')}: {citationData.chapterTitle}</div>
)}
{citationData.editor && (
{citationData.editor && citationData.editor.trim() !== '' && (
<div>{t('Edited by')} {citationData.editor}</div>
)}
{citationData.publishedIn && (
{citationData.publishedIn && citationData.publishedIn.trim() !== '' && (
<div>
{t('Published in')} {citationData.publishedIn}
{citationData.volume && `, ${t('Volume')} ${citationData.volume}`}
{citationData.volume && citationData.volume.trim() !== '' ? `, ${t('Volume')} ${citationData.volume}` : ''}
</div>
)}
{citationData.publishedBy && (
{citationData.publishedBy && citationData.publishedBy.trim() !== '' && (
<div>{citationData.publishedBy}</div>
)}
{citationData.publishedOn && (
<div className="text-muted-foreground">{formatDate(citationData.publishedOn)}</div>
{citationData.publishedOn && citationData.publishedOn.trim() !== '' && (
<div className="text-muted-foreground">{t('Published on')} {formatDate(citationData.publishedOn)}</div>
)}
{citationData.pageRange && (
{citationData.pageRange && citationData.pageRange.trim() !== '' && (
<div className="text-muted-foreground">{t('Pages')}: {citationData.pageRange}</div>
)}
{citationData.doi && (
{citationData.doi && citationData.doi.trim() !== '' && (
<div className="text-xs text-muted-foreground">DOI: {citationData.doi}</div>
)}
{citationData.accessedOn && (
{citationData.accessedOn && citationData.accessedOn.trim() !== '' && (
<div className="text-xs text-muted-foreground">
{t('Accessed on')} {formatDate(citationData.accessedOn)}
</div>
)}
{citationData.version && (
{citationData.version && citationData.version.trim() !== '' && (
<div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div>
)}
{citationData.summary && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div>
{citationData.location && citationData.location.trim() !== '' && (
<div className="text-xs text-muted-foreground">
{t('Location')}: {citationData.location}
</div>
)}
{event.content && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary">
{citationData.geohash && citationData.geohash.trim() !== '' && (
<div className="text-xs text-muted-foreground font-mono">
{t('Geohash')}: {citationData.geohash}
</div>
)}
{citationData.summary && citationData.summary.trim() !== '' && (
<div className="text-muted-foreground mt-2 whitespace-pre-wrap">{citationData.summary}</div>
)}
{event.content && event.content.trim() !== '' && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary whitespace-pre-wrap">
{event.content}
</div>
)}
@ -263,10 +534,10 @@ export default function CitationCard({ event, className, displayType = 'end' }: @@ -263,10 +534,10 @@ export default function CitationCard({ event, className, displayType = 'end' }:
</div>
)}
{citationData.summary && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div>
<div className="text-muted-foreground mt-2 whitespace-pre-wrap">{citationData.summary}</div>
)}
{event.content && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary">
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary whitespace-pre-wrap">
{event.content}
</div>
)}
@ -277,42 +548,24 @@ export default function CitationCard({ event, className, displayType = 'end' }: @@ -277,42 +548,24 @@ export default function CitationCard({ event, className, displayType = 'end' }:
return null
}
const getIcon = () => {
switch (citationData.type) {
case 'internal':
return <FileText className="w-4 h-4" />
case 'external':
return <ExternalLink className="w-4 h-4" />
case 'hardcopy':
return <Book className="w-4 h-4" />
case 'prompt':
return <Bot className="w-4 h-4" />
default:
return <FileText className="w-4 h-4" />
}
}
const getTitle = () => {
switch (citationData.type) {
case 'internal':
return t('Internal Citation')
case 'external':
return t('External Citation')
case 'hardcopy':
return t('Hardcopy Citation')
case 'prompt':
return t('Prompt Citation')
default:
return t('Citation')
}
}
// For inline citations, render a compact version
// For inline citations, render a compact version in academic format
if (displayType === 'inline' || displayType === 'prompt-inline') {
const inlineText = citationData.type === 'internal' && citationData.author && citationData.publishedOn
? `(${citationData.author}, ${formatDate(citationData.publishedOn)})`
: citationData.type === 'prompt' && citationData.llm
? `(${citationData.llm})`
// APA format: (Author, Year)
const author = citationData.type === 'internal' || citationData.type === 'external' || citationData.type === 'hardcopy'
? citationData.author
: citationData.type === 'prompt'
? citationData.llm
: ''
const year = formatYear(
citationData.publishedOn || citationData.accessedOn || ''
)
const inlineText = author && year
? `(${author}, ${year})`
: author
? `(${author})`
: `[${t('Citation')}]`
return (
@ -335,55 +588,59 @@ export default function CitationCard({ event, className, displayType = 'end' }: @@ -335,55 +588,59 @@ export default function CitationCard({ event, className, displayType = 'end' }:
)
}
// For footnotes (foot-end), render a brief reference
// For footnotes (foot-end), render a brief academic reference
if (displayType === 'foot-end') {
const academicText = formatAcademicCitation()
return (
<div className={className}>
<div className="text-sm text-muted-foreground">
{citationData.type === 'internal' && citationData.author && citationData.publishedOn
? `${citationData.author}, ${formatDate(citationData.publishedOn)}`
: citationData.type === 'external' && citationData.author
? `${citationData.author}`
: citationData.type === 'hardcopy' && citationData.author
? `${citationData.author}`
: citationData.type === 'prompt' && citationData.llm
? `${citationData.llm}`
: t('See reference')}
<div className="text-sm">
{academicText || t('See reference')}
</div>
</div>
)
}
// For quotes, render with quote styling
if (displayType === 'quote') {
// For footnotes (foot), render full citation information (same as endnotes)
if (displayType === 'foot') {
return (
<Card className={className}>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
{getIcon()}
{getTitle()}
</CardTitle>
</CardHeader>
<CardContent>
<div className={className}>
<div className="text-sm leading-relaxed">
{renderCitationContent()}
</CardContent>
</Card>
</div>
</div>
)
}
// For endnotes, footnotes, and prompt-end, render full citation
// For endnotes and prompt-end, render full citation information (no card UI)
if (displayType === 'end' || displayType === 'prompt-end') {
return (
<Card className={className}>
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
{getIcon()}
{getTitle()}
</CardTitle>
</CardHeader>
<CardContent>
<div className={className}>
<div className="text-sm leading-relaxed">
{renderCitationContent()}
</CardContent>
</Card>
</div>
</div>
)
}
// For quotes (block-level), render full citation information in block quote format
if (displayType === 'quote') {
return (
<blockquote className={`${className} border-l-4 border-gray-300 dark:border-gray-600 pl-6 py-2 my-4 text-sm`}>
<div className="leading-relaxed">
{renderCitationContent()}
</div>
</blockquote>
)
}
// Default: render in academic format
const academicText = formatAcademicCitation()
return (
<div className={className}>
<div className="text-sm leading-relaxed">
{academicText || t('Citation')}
</div>
</div>
)
}

2
src/components/EmbeddedCitation/index.tsx

@ -55,6 +55,6 @@ export default function EmbeddedCitation({ citationId, displayType = 'end', clas @@ -55,6 +55,6 @@ export default function EmbeddedCitation({ citationId, displayType = 'end', clas
)
}
return <CitationCard event={event} displayType={displayType} className={className} />
return <CitationCard event={event} displayType={displayType} className={className} citationId={citationId} />
}

117
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -1223,26 +1223,26 @@ export default function AsciidocArticle({ @@ -1223,26 +1223,26 @@ export default function AsciidocArticle({
footnotesSection.appendChild(h3)
const ol = document.createElement('ol')
ol.className = 'list-decimal list-inside space-y-2'
// Academic style: proper list formatting with aligned numbers
ol.className = 'list-decimal pl-6 space-y-3'
ol.style.listStylePosition = 'outside'
footnotes.forEach((citation) => {
const li = document.createElement('li')
li.id = citation.id
li.className = 'text-sm'
const span = document.createElement('span')
span.className = 'font-semibold'
span.textContent = `[${citation.index + 1}]: `
li.appendChild(span)
li.className = 'text-sm pl-2'
li.style.display = 'list-item'
const citationContainer = document.createElement('span')
citationContainer.className = 'inline-block mt-1'
citationContainer.className = 'inline'
li.appendChild(citationContainer)
const backLink = document.createElement('a')
backLink.href = `#citation-ref-${citation.index}`
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-1'
backLink.textContent = '↩'
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
backLink.setAttribute('aria-label', 'Return to citation')
// Use hyperlink icon instead of emoji
backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
backLink.addEventListener('click', (e) => {
e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.index}`)
@ -1254,8 +1254,13 @@ export default function AsciidocArticle({ @@ -1254,8 +1254,13 @@ export default function AsciidocArticle({
ol.appendChild(li)
// Render citation component
const citationRoot = createRoot(citationContainer)
// Render citation component - use a wrapper div to position backlink
const citationWrapperDiv = document.createElement('div')
citationWrapperDiv.className = 'inline'
citationWrapperDiv.style.display = 'inline'
citationContainer.appendChild(citationWrapperDiv)
const citationRoot = createRoot(citationWrapperDiv)
citationRoot.render(
<DeletedEventProvider>
<ReplyProvider>
@ -1266,7 +1271,22 @@ export default function AsciidocArticle({ @@ -1266,7 +1271,22 @@ export default function AsciidocArticle({
</ReplyProvider>
</DeletedEventProvider>
)
reactRootsRef.current.set(citationContainer, citationRoot)
reactRootsRef.current.set(citationWrapperDiv, citationRoot)
// Insert backlink at end of first line after citation renders
setTimeout(() => {
const firstDiv = citationWrapperDiv.querySelector('div:first-child') as HTMLElement
if (firstDiv) {
firstDiv.style.display = 'inline'
firstDiv.style.position = 'relative'
backLink.style.position = 'absolute'
backLink.style.right = '0'
backLink.style.top = '0'
firstDiv.appendChild(backLink)
} else {
citationWrapperDiv.appendChild(backLink)
}
}, 100)
})
footnotesSection.appendChild(ol)
@ -1298,26 +1318,29 @@ export default function AsciidocArticle({ @@ -1298,26 +1318,29 @@ export default function AsciidocArticle({
referencesSection.appendChild(h3)
const ol = document.createElement('ol')
ol.className = 'list-decimal list-inside space-y-2'
// Academic style: proper list formatting with aligned numbers
ol.className = 'list-decimal pl-6 space-y-3'
ol.style.listStylePosition = 'outside'
endCitations.forEach((citation) => {
const li = document.createElement('li')
li.id = `citation-end-${citation.index}`
li.className = 'text-sm'
li.className = 'text-sm pl-2'
li.style.display = 'list-item'
const span = document.createElement('span')
span.className = 'font-semibold'
span.textContent = `[${citation.index + 1}]: `
li.appendChild(span)
const citationWrapper = document.createElement('div')
citationWrapper.className = 'inline-block w-full relative'
li.appendChild(citationWrapper)
const citationContainer = document.createElement('span')
citationContainer.className = 'inline-block mt-1'
li.appendChild(citationContainer)
citationContainer.className = 'inline'
citationWrapper.appendChild(citationContainer)
const backLink = document.createElement('a')
backLink.href = `#citation-ref-${citation.index}`
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-1'
backLink.textContent = '↩'
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
backLink.setAttribute('aria-label', 'Return to citation')
backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
backLink.addEventListener('click', (e) => {
e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.index}`)
@ -1325,7 +1348,6 @@ export default function AsciidocArticle({ @@ -1325,7 +1348,6 @@ export default function AsciidocArticle({
refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
li.appendChild(backLink)
ol.appendChild(li)
@ -1342,6 +1364,21 @@ export default function AsciidocArticle({ @@ -1342,6 +1364,21 @@ export default function AsciidocArticle({
</DeletedEventProvider>
)
reactRootsRef.current.set(citationContainer, citationRoot)
// Insert backlink at end of first line after citation renders
setTimeout(() => {
const firstDiv = citationContainer.querySelector('div:first-child') as HTMLElement
if (firstDiv) {
firstDiv.style.display = 'inline'
firstDiv.style.position = 'relative'
backLink.style.position = 'absolute'
backLink.style.right = '0'
backLink.style.top = '0'
firstDiv.appendChild(backLink)
} else {
citationWrapper.appendChild(backLink)
}
}, 100)
})
referencesSection.appendChild(ol)
@ -1778,6 +1815,38 @@ export default function AsciidocArticle({ @@ -1778,6 +1815,38 @@ export default function AsciidocArticle({
font-size: 0.83em;
line-height: 0;
}
/* Academic references section formatting */
.asciidoc-content #references-section ol,
.asciidoc-content #footnotes-section ol {
list-style: decimal;
padding-left: 1.5rem;
list-style-position: outside;
}
.asciidoc-content #references-section li,
.asciidoc-content #footnotes-section li {
padding-left: 0.5rem;
margin-bottom: 0.5rem;
line-height: 1.6;
display: list-item;
}
/* Position backlink at end of first line */
.asciidoc-content #references-section li > div > span > div:first-child,
.asciidoc-content #footnotes-section li > div > span > div:first-child {
position: relative;
display: inline-block;
width: 100%;
}
.asciidoc-content #references-section h3,
.asciidoc-content #footnotes-section h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
}
/* Blockquote spacing in citations */
.asciidoc-content #references-section blockquote,
.asciidoc-content #footnotes-section blockquote {
padding-left: 1.5rem !important;
}
`}</style>
<div className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}>
{/* Metadata */}

35
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -2509,23 +2509,25 @@ function parseMarkdownContent( @@ -2509,23 +2509,25 @@ function parseMarkdownContent(
wrappedParts.push(
<div key="citations-footnotes-section" className="mt-8 pt-4 border-t border-gray-300 dark:border-gray-700">
<h3 className="text-lg font-semibold mb-4">Citations</h3>
<ol className="list-decimal list-inside space-y-2">
<ol className="list-decimal pl-6 space-y-3" style={{ listStylePosition: 'outside' }}>
{footCitations.map((citation, idx) => (
<li
key={`citation-footnote-${idx}`}
id={`citation-${citation.id.replace('citation-', '')}`}
className="text-sm"
className="text-sm pl-2"
>
<span className="font-semibold">[{idx + 1}]:</span>{' '}
<div className="inline-block w-full relative">
<span className="inline">
<EmbeddedCitation
citationId={citation.citationId}
displayType={citation.type as 'foot' | 'foot-end'}
className="inline-block mt-1"
className="inline"
/>
{' '}
</span>
<a
href={`#citation-ref-${citation.id.replace('citation-', '')}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs"
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center absolute right-0 top-0"
aria-label="Return to citation"
onClick={(e) => {
e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`)
@ -2534,8 +2536,9 @@ function parseMarkdownContent( @@ -2534,8 +2536,9 @@ function parseMarkdownContent(
}
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>
</div>
</li>
))}
</ol>
@ -2549,23 +2552,26 @@ function parseMarkdownContent( @@ -2549,23 +2552,26 @@ function parseMarkdownContent(
wrappedParts.push(
<div key="references-section" id="references-section" className="mt-8 pt-4 border-t border-gray-300 dark:border-gray-700">
<h3 className="text-lg font-semibold mb-4">References</h3>
<ol className="list-decimal list-inside space-y-2">
<ol className="list-decimal pl-6 space-y-3" style={{ listStylePosition: 'outside' }}>
{endCitations.map((citation, idx) => (
<li
key={`citation-end-${idx}`}
id={`citation-end-${idx}`}
className="text-sm"
className="text-sm pl-2"
style={{ display: 'list-item' }}
>
<span className="font-semibold">[{idx + 1}]:</span>{' '}
<div className="inline-block w-full relative">
<span className="inline">
<EmbeddedCitation
citationId={citation.citationId}
displayType={citation.type as 'end' | 'prompt-end'}
className="inline-block mt-1"
className="inline"
/>
{' '}
</span>
<a
href={`#citation-ref-${citation.id.replace('citation-', '')}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs"
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center absolute right-0 top-0"
aria-label="Return to citation"
onClick={(e) => {
e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`)
@ -2574,8 +2580,9 @@ function parseMarkdownContent( @@ -2574,8 +2580,9 @@ function parseMarkdownContent(
}
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>
</div>
</li>
))}
</ol>

33
src/components/PostEditor/PostContent.tsx

@ -580,14 +580,26 @@ export default function PostContent({ @@ -580,14 +580,26 @@ export default function PostContent({
summary: citationSummary.trim() || undefined
})
} else if (isCitationHardcopy) {
return createCitationHardcopyDraftEvent(cleanedText, {
accessedOn: citationAccessedOn.trim() || new Date().toISOString(),
// Convert date strings to ISO 8601 format if they exist
const formatDateToISO = (dateStr: string): string => {
if (!dateStr || !dateStr.trim()) return ''
// If already in ISO format, return as is
if (dateStr.includes('T')) return dateStr
// If in YYYY-MM-DD format, convert to ISO
if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) {
return new Date(dateStr + 'T00:00:00Z').toISOString()
}
return dateStr
}
const hardcopyOptions = {
accessedOn: formatDateToISO(citationAccessedOn.trim()) || new Date().toISOString(),
title: citationTitle.trim() || undefined,
author: citationAuthor.trim() || undefined,
pageRange: citationHardcopyPageRange.trim() || undefined,
chapterTitle: citationHardcopyChapterTitle.trim() || undefined,
editor: citationHardcopyEditor.trim() || undefined,
publishedOn: citationPublishedOn.trim() || undefined,
publishedOn: citationPublishedOn.trim() ? formatDateToISO(citationPublishedOn.trim()) : undefined,
publishedBy: citationPublishedBy.trim() || undefined,
publishedIn: citationHardcopyPublishedIn.trim() || undefined,
volume: citationHardcopyVolume.trim() || undefined,
@ -596,7 +608,12 @@ export default function PostContent({ @@ -596,7 +608,12 @@ export default function PostContent({
location: citationLocation.trim() || undefined,
geohash: citationGeohash.trim() || undefined,
summary: citationSummary.trim() || undefined
})
}
// Debug: Log what we're passing to the function
console.log('Creating hardcopy citation with options:', hardcopyOptions)
return createCitationHardcopyDraftEvent(cleanedText, hardcopyOptions)
} else if (isCitationPrompt) {
return createCitationPromptDraftEvent(cleanedText, {
llm: citationPromptLlm.trim(),
@ -1641,13 +1658,14 @@ export default function PostContent({ @@ -1641,13 +1658,14 @@ export default function PostContent({
{/* Citation metadata fields */}
{(isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt) && (
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="p-4 border rounded-lg bg-muted/30">
<div className="text-sm font-medium mb-3">
{isCitationInternal && t('Internal Citation Settings')}
{isCitationExternal && t('External Citation Settings')}
{isCitationHardcopy && t('Hardcopy Citation Settings')}
{isCitationPrompt && t('Prompt Citation Settings')}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Prompt Citation specific fields - shown first if prompt */}
{isCitationPrompt && (
@ -1939,8 +1957,8 @@ export default function PostContent({ @@ -1939,8 +1957,8 @@ export default function PostContent({
/>
</div>
{/* Summary field - different label for prompt citations */}
<div className="space-y-2">
{/* Summary field - different label for prompt citations - spans full width on desktop */}
<div className="space-y-2 md:col-span-2">
<Label htmlFor="citation-summary" className="text-sm font-medium">
{isCitationPrompt ? t('Prompt Conversation Script') : t('Summary')}
</Label>
@ -1982,6 +2000,7 @@ export default function PostContent({ @@ -1982,6 +2000,7 @@ export default function PostContent({
</>
)}
</div>
</div>
)}
<PostTextarea

Loading…
Cancel
Save