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. 95
      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'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { ExternalLink } from 'lucide-react'
import { ExternalLink, Book, FileText, Bot } from 'lucide-react' import { nip19 } from 'nostr-tools'
function getTagValue(event: Event, tagName: string): string { function getTagValue(event: Event, tagName: string): string {
return event.tags.find(tag => tag[0] === tagName)?.[1] || '' return event.tags.find(tag => tag[0] === tagName)?.[1] || ''
@ -13,9 +13,10 @@ interface CitationCardProps {
event: Event event: Event
className?: string className?: string
displayType?: 'end' | 'foot' | 'foot-end' | 'inline' | 'quote' | 'prompt-end' | 'prompt-inline' 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 { t } = useTranslation()
const citationData = useMemo(() => { const citationData = useMemo(() => {
@ -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 = () => { const renderCitationContent = () => {
if (citationData.type === 'internal') { 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 ( return (
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
{citationData.author && ( {/* Main citation line: Author. Nostr: "Title". Published on. */}
<div className="font-semibold">{citationData.author}</div> <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.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.title && ( {citationData.location && (
<div className="italic">"{citationData.title}"</div> <div className="text-xs text-muted-foreground">
{t('Location')}: {citationData.location}
</div>
)} )}
{citationData.publishedOn && ( {citationData.geohash && (
<div className="text-muted-foreground">{formatDate(citationData.publishedOn)}</div> <div className="text-xs text-muted-foreground font-mono">
{t('Geohash')}: {citationData.geohash}
</div>
)} )}
{citationData.cTag && ( {citationData.relayHint && (
<div className="text-xs text-muted-foreground font-mono break-all"> <div className="text-xs text-muted-foreground">
nostr:{citationData.cTag} {t('Relay')}: {citationData.relayHint}
</div> </div>
)} )}
{citationData.summary && ( {citationData.summary && citationData.title && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div> <div className="text-muted-foreground mt-2 text-xs whitespace-pre-wrap">{citationData.summary}</div>
)} )}
{event.content && ( {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} {event.content}
</div> </div>
)} )}
@ -181,62 +405,109 @@ export default function CitationCard({ event, className, displayType = 'end' }:
{citationData.version && ( {citationData.version && (
<div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div> <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 && ( {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 && ( {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} {event.content}
</div> </div>
)} )}
</div> </div>
) )
} else if (citationData.type === 'hardcopy') { } 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 ( return (
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
{citationData.author && ( {citationData.author && citationData.author.trim() !== '' && (
<div className="font-semibold">{citationData.author}</div> <div className="font-semibold">{citationData.author}</div>
)} )}
{citationData.title && ( {citationData.title && citationData.title.trim() !== '' && (
<div className="italic">"{citationData.title}"</div> <div className="italic">"{citationData.title}"</div>
)} )}
{citationData.chapterTitle && ( {citationData.chapterTitle && citationData.chapterTitle.trim() !== '' && (
<div className="text-muted-foreground">{t('Chapter')}: {citationData.chapterTitle}</div> <div className="text-muted-foreground">{t('Chapter')}: {citationData.chapterTitle}</div>
)} )}
{citationData.editor && ( {citationData.editor && citationData.editor.trim() !== '' && (
<div>{t('Edited by')} {citationData.editor}</div> <div>{t('Edited by')} {citationData.editor}</div>
)} )}
{citationData.publishedIn && ( {citationData.publishedIn && citationData.publishedIn.trim() !== '' && (
<div> <div>
{t('Published in')} {citationData.publishedIn} {t('Published in')} {citationData.publishedIn}
{citationData.volume && `, ${t('Volume')} ${citationData.volume}`} {citationData.volume && citationData.volume.trim() !== '' ? `, ${t('Volume')} ${citationData.volume}` : ''}
</div> </div>
)} )}
{citationData.publishedBy && ( {citationData.publishedBy && citationData.publishedBy.trim() !== '' && (
<div>{citationData.publishedBy}</div> <div>{citationData.publishedBy}</div>
)} )}
{citationData.publishedOn && ( {citationData.publishedOn && citationData.publishedOn.trim() !== '' && (
<div className="text-muted-foreground">{formatDate(citationData.publishedOn)}</div> <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> <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> <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"> <div className="text-xs text-muted-foreground">
{t('Accessed on')} {formatDate(citationData.accessedOn)} {t('Accessed on')} {formatDate(citationData.accessedOn)}
</div> </div>
)} )}
{citationData.version && ( {citationData.version && citationData.version.trim() !== '' && (
<div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div> <div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div>
)} )}
{citationData.summary && ( {citationData.location && citationData.location.trim() !== '' && (
<div className="text-muted-foreground mt-2">{citationData.summary}</div> <div className="text-xs text-muted-foreground">
{t('Location')}: {citationData.location}
</div>
)} )}
{event.content && ( {citationData.geohash && citationData.geohash.trim() !== '' && (
<div className="mt-2 p-2 bg-muted/50 rounded text-sm border-l-2 border-primary"> <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} {event.content}
</div> </div>
)} )}
@ -263,10 +534,10 @@ export default function CitationCard({ event, className, displayType = 'end' }:
</div> </div>
)} )}
{citationData.summary && ( {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 && ( {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} {event.content}
</div> </div>
)} )}
@ -277,42 +548,24 @@ export default function CitationCard({ event, className, displayType = 'end' }:
return null 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') { if (displayType === 'inline' || displayType === 'prompt-inline') {
const inlineText = citationData.type === 'internal' && citationData.author && citationData.publishedOn // APA format: (Author, Year)
? `(${citationData.author}, ${formatDate(citationData.publishedOn)})` const author = citationData.type === 'internal' || citationData.type === 'external' || citationData.type === 'hardcopy'
: citationData.type === 'prompt' && citationData.llm ? citationData.author
? `(${citationData.llm})` : citationData.type === 'prompt'
? citationData.llm
: ''
const year = formatYear(
citationData.publishedOn || citationData.accessedOn || ''
)
const inlineText = author && year
? `(${author}, ${year})`
: author
? `(${author})`
: `[${t('Citation')}]` : `[${t('Citation')}]`
return ( return (
@ -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') { if (displayType === 'foot-end') {
const academicText = formatAcademicCitation()
return ( return (
<div className={className}> <div className={className}>
<div className="text-sm text-muted-foreground"> <div className="text-sm">
{citationData.type === 'internal' && citationData.author && citationData.publishedOn {academicText || t('See reference')}
? `${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> </div>
</div> </div>
) )
} }
// For quotes, render with quote styling // For footnotes (foot), render full citation information (same as endnotes)
if (displayType === 'foot') {
return (
<div className={className}>
<div className="text-sm leading-relaxed">
{renderCitationContent()}
</div>
</div>
)
}
// For endnotes and prompt-end, render full citation information (no card UI)
if (displayType === 'end' || displayType === 'prompt-end') {
return (
<div className={className}>
<div className="text-sm leading-relaxed">
{renderCitationContent()}
</div>
</div>
)
}
// For quotes (block-level), render full citation information in block quote format
if (displayType === 'quote') { if (displayType === 'quote') {
return ( return (
<Card className={className}> <blockquote className={`${className} border-l-4 border-gray-300 dark:border-gray-600 pl-6 py-2 my-4 text-sm`}>
<CardHeader className="pb-2"> <div className="leading-relaxed">
<CardTitle className="text-sm flex items-center gap-2">
{getIcon()}
{getTitle()}
</CardTitle>
</CardHeader>
<CardContent>
{renderCitationContent()} {renderCitationContent()}
</CardContent> </div>
</Card> </blockquote>
) )
} }
// For endnotes, footnotes, and prompt-end, render full citation // Default: render in academic format
const academicText = formatAcademicCitation()
return ( return (
<Card className={className}> <div className={className}>
<CardHeader className="pb-2"> <div className="text-sm leading-relaxed">
<CardTitle className="text-sm flex items-center gap-2"> {academicText || t('Citation')}
{getIcon()} </div>
{getTitle()} </div>
</CardTitle>
</CardHeader>
<CardContent>
{renderCitationContent()}
</CardContent>
</Card>
) )
} }

2
src/components/EmbeddedCitation/index.tsx

@ -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({
footnotesSection.appendChild(h3) footnotesSection.appendChild(h3)
const ol = document.createElement('ol') 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) => { footnotes.forEach((citation) => {
const li = document.createElement('li') const li = document.createElement('li')
li.id = citation.id li.id = citation.id
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 citationContainer = document.createElement('span') const citationContainer = document.createElement('span')
citationContainer.className = 'inline-block mt-1' citationContainer.className = 'inline'
li.appendChild(citationContainer) li.appendChild(citationContainer)
const backLink = document.createElement('a') const backLink = document.createElement('a')
backLink.href = `#citation-ref-${citation.index}` 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.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.textContent = '↩' 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) => { backLink.addEventListener('click', (e) => {
e.preventDefault() e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.index}`) const refElement = document.getElementById(`citation-ref-${citation.index}`)
@ -1254,8 +1254,13 @@ export default function AsciidocArticle({
ol.appendChild(li) ol.appendChild(li)
// Render citation component // Render citation component - use a wrapper div to position backlink
const citationRoot = createRoot(citationContainer) const citationWrapperDiv = document.createElement('div')
citationWrapperDiv.className = 'inline'
citationWrapperDiv.style.display = 'inline'
citationContainer.appendChild(citationWrapperDiv)
const citationRoot = createRoot(citationWrapperDiv)
citationRoot.render( citationRoot.render(
<DeletedEventProvider> <DeletedEventProvider>
<ReplyProvider> <ReplyProvider>
@ -1266,7 +1271,22 @@ export default function AsciidocArticle({
</ReplyProvider> </ReplyProvider>
</DeletedEventProvider> </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) footnotesSection.appendChild(ol)
@ -1298,26 +1318,29 @@ export default function AsciidocArticle({
referencesSection.appendChild(h3) referencesSection.appendChild(h3)
const ol = document.createElement('ol') 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) => { endCitations.forEach((citation) => {
const li = document.createElement('li') const li = document.createElement('li')
li.id = `citation-end-${citation.index}` 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') const citationWrapper = document.createElement('div')
span.className = 'font-semibold' citationWrapper.className = 'inline-block w-full relative'
span.textContent = `[${citation.index + 1}]: ` li.appendChild(citationWrapper)
li.appendChild(span)
const citationContainer = document.createElement('span') const citationContainer = document.createElement('span')
citationContainer.className = 'inline-block mt-1' citationContainer.className = 'inline'
li.appendChild(citationContainer) citationWrapper.appendChild(citationContainer)
const backLink = document.createElement('a') const backLink = document.createElement('a')
backLink.href = `#citation-ref-${citation.index}` 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.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.textContent = '↩' 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) => { backLink.addEventListener('click', (e) => {
e.preventDefault() e.preventDefault()
const refElement = document.getElementById(`citation-ref-${citation.index}`) const refElement = document.getElementById(`citation-ref-${citation.index}`)
@ -1325,7 +1348,6 @@ export default function AsciidocArticle({
refElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
} }
}) })
li.appendChild(backLink)
ol.appendChild(li) ol.appendChild(li)
@ -1342,6 +1364,21 @@ export default function AsciidocArticle({
</DeletedEventProvider> </DeletedEventProvider>
) )
reactRootsRef.current.set(citationContainer, citationRoot) 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) referencesSection.appendChild(ol)
@ -1778,6 +1815,38 @@ export default function AsciidocArticle({
font-size: 0.83em; font-size: 0.83em;
line-height: 0; 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> `}</style>
<div className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}> <div className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}>
{/* Metadata */} {/* Metadata */}

95
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -2509,33 +2509,36 @@ function parseMarkdownContent(
wrappedParts.push( wrappedParts.push(
<div key="citations-footnotes-section" className="mt-8 pt-4 border-t border-gray-300 dark:border-gray-700"> <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> <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) => ( {footCitations.map((citation, idx) => (
<li <li
key={`citation-footnote-${idx}`} key={`citation-footnote-${idx}`}
id={`citation-${citation.id.replace('citation-', '')}`} 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">
<EmbeddedCitation <span className="inline">
citationId={citation.citationId} <EmbeddedCitation
displayType={citation.type as 'foot' | 'foot-end'} citationId={citation.citationId}
className="inline-block mt-1" displayType={citation.type as 'foot' | 'foot-end'}
/> className="inline"
{' '} />
<a </span>
href={`#citation-ref-${citation.id.replace('citation-', '')}`} <a
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs" href={`#citation-ref-${citation.id.replace('citation-', '')}`}
onClick={(e) => { 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"
e.preventDefault() aria-label="Return to citation"
const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`) onClick={(e) => {
if (refElement) { e.preventDefault()
refElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`)
} if (refElement) {
}} refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
> }
}}
</a> >
<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> </li>
))} ))}
</ol> </ol>
@ -2549,33 +2552,37 @@ function parseMarkdownContent(
wrappedParts.push( wrappedParts.push(
<div key="references-section" id="references-section" className="mt-8 pt-4 border-t border-gray-300 dark:border-gray-700"> <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> <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) => ( {endCitations.map((citation, idx) => (
<li <li
key={`citation-end-${idx}`} key={`citation-end-${idx}`}
id={`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">
<EmbeddedCitation <span className="inline">
citationId={citation.citationId} <EmbeddedCitation
displayType={citation.type as 'end' | 'prompt-end'} citationId={citation.citationId}
className="inline-block mt-1" displayType={citation.type as 'end' | 'prompt-end'}
/> className="inline"
{' '} />
<a </span>
href={`#citation-ref-${citation.id.replace('citation-', '')}`} <a
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs" href={`#citation-ref-${citation.id.replace('citation-', '')}`}
onClick={(e) => { 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"
e.preventDefault() aria-label="Return to citation"
const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`) onClick={(e) => {
if (refElement) { e.preventDefault()
refElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) const refElement = document.getElementById(`citation-ref-${citation.id.replace('citation-', '')}`)
} if (refElement) {
}} refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
> }
}}
</a> >
<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> </li>
))} ))}
</ol> </ol>

33
src/components/PostEditor/PostContent.tsx

@ -580,14 +580,26 @@ export default function PostContent({
summary: citationSummary.trim() || undefined summary: citationSummary.trim() || undefined
}) })
} else if (isCitationHardcopy) { } else if (isCitationHardcopy) {
return createCitationHardcopyDraftEvent(cleanedText, { // Convert date strings to ISO 8601 format if they exist
accessedOn: citationAccessedOn.trim() || new Date().toISOString(), 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, title: citationTitle.trim() || undefined,
author: citationAuthor.trim() || undefined, author: citationAuthor.trim() || undefined,
pageRange: citationHardcopyPageRange.trim() || undefined, pageRange: citationHardcopyPageRange.trim() || undefined,
chapterTitle: citationHardcopyChapterTitle.trim() || undefined, chapterTitle: citationHardcopyChapterTitle.trim() || undefined,
editor: citationHardcopyEditor.trim() || undefined, editor: citationHardcopyEditor.trim() || undefined,
publishedOn: citationPublishedOn.trim() || undefined, publishedOn: citationPublishedOn.trim() ? formatDateToISO(citationPublishedOn.trim()) : undefined,
publishedBy: citationPublishedBy.trim() || undefined, publishedBy: citationPublishedBy.trim() || undefined,
publishedIn: citationHardcopyPublishedIn.trim() || undefined, publishedIn: citationHardcopyPublishedIn.trim() || undefined,
volume: citationHardcopyVolume.trim() || undefined, volume: citationHardcopyVolume.trim() || undefined,
@ -596,7 +608,12 @@ export default function PostContent({
location: citationLocation.trim() || undefined, location: citationLocation.trim() || undefined,
geohash: citationGeohash.trim() || undefined, geohash: citationGeohash.trim() || undefined,
summary: citationSummary.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) { } else if (isCitationPrompt) {
return createCitationPromptDraftEvent(cleanedText, { return createCitationPromptDraftEvent(cleanedText, {
llm: citationPromptLlm.trim(), llm: citationPromptLlm.trim(),
@ -1641,13 +1658,14 @@ export default function PostContent({
{/* Citation metadata fields */} {/* Citation metadata fields */}
{(isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt) && ( {(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"> <div className="text-sm font-medium mb-3">
{isCitationInternal && t('Internal Citation Settings')} {isCitationInternal && t('Internal Citation Settings')}
{isCitationExternal && t('External Citation Settings')} {isCitationExternal && t('External Citation Settings')}
{isCitationHardcopy && t('Hardcopy Citation Settings')} {isCitationHardcopy && t('Hardcopy Citation Settings')}
{isCitationPrompt && t('Prompt Citation Settings')} {isCitationPrompt && t('Prompt Citation Settings')}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Prompt Citation specific fields - shown first if prompt */} {/* Prompt Citation specific fields - shown first if prompt */}
{isCitationPrompt && ( {isCitationPrompt && (
@ -1939,8 +1957,8 @@ export default function PostContent({
/> />
</div> </div>
{/* Summary field - different label for prompt citations */} {/* Summary field - different label for prompt citations - spans full width on desktop */}
<div className="space-y-2"> <div className="space-y-2 md:col-span-2">
<Label htmlFor="citation-summary" className="text-sm font-medium"> <Label htmlFor="citation-summary" className="text-sm font-medium">
{isCitationPrompt ? t('Prompt Conversation Script') : t('Summary')} {isCitationPrompt ? t('Prompt Conversation Script') : t('Summary')}
</Label> </Label>
@ -1981,6 +1999,7 @@ export default function PostContent({
</div> </div>
</> </>
)} )}
</div>
</div> </div>
)} )}

Loading…
Cancel
Save