diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/model/impl/TextBlock.java b/vspace/src/main/java/edu/asu/diging/vspace/core/model/impl/TextBlock.java index 2ffb5a31b..838b42a0d 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/core/model/impl/TextBlock.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/model/impl/TextBlock.java @@ -9,6 +9,7 @@ import org.commonmark.renderer.html.HtmlRenderer; import edu.asu.diging.vspace.core.model.ITextBlock; +import edu.asu.diging.vspace.core.util.CitationFormatter; @Entity public class TextBlock extends ContentBlock implements ITextBlock { @@ -46,9 +47,58 @@ public void setText(String text) { @Override @Transient public String htmlRenderedText() { + String processedText = text; + + // First, format citations in the text + if (processedText != null) { + processedText = CitationFormatter.formatCitations(processedText); + } + + // Then process with Markdown Parser parser = Parser.builder().build(); - Node document = parser.parse(text); + Node document = parser.parse(processedText); HtmlRenderer renderer = HtmlRenderer.builder().build(); - return renderer.render(document); + String htmlOutput = renderer.render(document); + + // Add citation-specific CSS classes for styling + htmlOutput = addCitationStyling(htmlOutput); + + return htmlOutput; + } + + /** + * Adds CSS classes to citation elements for proper styling + */ + @Transient + private String addCitationStyling(String html) { + if (html == null) { + return null; + } + + // Add citation class to parenthetical citations + html = html.replaceAll("\\(([^)]+,\\s*\\d{4}[^)]*)\\)", + "($1)"); + + // Add reference class to formatted references (those with italicized titles) + html = html.replaceAll("([^<]+[^<]*\\.)", + "
$1
"); + + return html; + } + + /** + * Gets the raw text with citation formatting applied but without HTML conversion + */ + @Transient + public String getFormattedText() { + return text != null ? CitationFormatter.formatCitations(text) : text; + } + + /** + * Validates if the text block contains properly formatted citations + */ + @Transient + public boolean hasValidCitations() { + return CitationFormatter.hasValidCitations(text); } } diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/util/CitationFormatter.java b/vspace/src/main/java/edu/asu/diging/vspace/core/util/CitationFormatter.java new file mode 100644 index 000000000..dafe0828c --- /dev/null +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/util/CitationFormatter.java @@ -0,0 +1,218 @@ +package edu.asu.diging.vspace.core.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CitationFormatter { + + // Pattern to match citation syntax: [@author year, pages] + private static final Pattern CITATION_PATTERN = Pattern.compile( + "\\[@([^,]+)\\s+(\\d{4})(?:,\\s*([^\\]]+))?\\]" + ); + + // Pattern to match full citation entries: {author, year, title, journal, etc.} + private static final Pattern FULL_CITATION_PATTERN = Pattern.compile( + "\\{([^}]+)\\}" + ); + + /** + * Formats text containing citation markers into proper APA format. + * + * @param text The input text containing citation markers + * @return Formatted text with proper citations + */ + public static String formatCitations(String text) { + if (text == null || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(); + String[] lines = text.split("\n"); + + for (String line : lines) { + result.append(formatLine(line)).append("\n"); + } + + return result.toString().trim(); + } + + /** + * Formats a single line of text with citations. + */ + private static String formatLine(String line) { + // Handle full citation entries (transform to reference list format) + line = formatFullCitations(line); + + // Handle in-text citations + line = formatInTextCitations(line); + + return line; + } + + /** + * Formats in-text citations like [@author 2020] to (Author, 2020) + */ + private static String formatInTextCitations(String text) { + Matcher matcher = CITATION_PATTERN.matcher(text); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + String author = matcher.group(1).trim(); + String year = matcher.group(2); + String pages = matcher.group(3); + + // Capitalize first letter of author's last name + author = capitalizeAuthor(author); + + String replacement; + if (pages != null && !pages.trim().isEmpty()) { + replacement = String.format("(%s, %s, %s)", author, year, pages.trim()); + } else { + replacement = String.format("(%s, %s)", author, year); + } + + matcher.appendReplacement(result, replacement); + } + matcher.appendTail(result); + + return result.toString(); + } + + /** + * Formats full citation entries into APA reference format + */ + private static String formatFullCitations(String text) { + Matcher matcher = FULL_CITATION_PATTERN.matcher(text); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + String citationData = matcher.group(1); + String formattedReference = parseAndFormatReference(citationData); + matcher.appendReplacement(result, formattedReference); + } + matcher.appendTail(result); + + return result.toString(); + } + + /** + * Parses citation data and formats it as APA reference + */ + private static String parseAndFormatReference(String citationData) { + String[] parts = citationData.split(","); + if (parts.length < 3) { + return citationData; // Return as-is if not enough parts + } + + String author; + String year; + String title; + int titleIndex; + + // Check if the first part looks like "LastName, FirstName" format + if (parts.length >= 4 && parts[1].trim().matches("^[A-Z][a-z]*\\.?$|^[A-Z][a-z]+$")) { + // Author is "LastName, FirstName" format (first two parts) + author = parts[0].trim() + ", " + parts[1].trim(); + year = parts[2].trim(); + title = parts[3].trim(); + titleIndex = 4; + } else { + // Author is just the first part + author = parts[0].trim(); + year = parts[1].trim(); + title = parts[2].trim(); + titleIndex = 3; + } + + // Basic APA format: Author, A. (Year). Title. + StringBuilder reference = new StringBuilder(); + reference.append(capitalizeAuthor(author)); + reference.append(" (").append(year).append("). "); + reference.append("*").append(title).append("*"); + + if (parts.length > titleIndex) { + String journal = parts[titleIndex].trim(); + reference.append(". ").append(journal); + } + + if (parts.length > titleIndex + 1) { + String pages = parts[titleIndex + 1].trim(); + reference.append(", ").append(pages); + } + + reference.append("."); + + return reference.toString(); + } + + /** + * Capitalizes author name properly for citations + */ + private static String capitalizeAuthor(String author) { + if (author == null || author.isEmpty()) { + return author; + } + + // Handle "et al." case - preserve it as is + if (author.toLowerCase().contains("et al")) { + return author; // Keep original formatting for et al. + } + + // Handle "lastname, firstname" format + if (author.contains(",")) { + String[] nameParts = author.split(","); + if (nameParts.length >= 2) { + String lastName = nameParts[0].trim(); + String firstName = nameParts[1].trim(); + return capitalizeFirstLetter(lastName) + ", " + + (firstName.length() > 0 ? Character.toUpperCase(firstName.charAt(0)) + "." : ""); + } + } + + // Handle "firstname lastname" format + String[] words = author.split("\\s+"); + if (words.length >= 2) { + String firstName = words[0]; + String lastName = words[words.length - 1]; + return capitalizeFirstLetter(lastName) + ", " + + (firstName.length() > 0 ? Character.toUpperCase(firstName.charAt(0)) + "." : ""); + } + + return capitalizeFirstLetter(author); + } + + /** + * Capitalizes the first letter of a string + */ + private static String capitalizeFirstLetter(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase(); + } + + /** + * Validates if text contains properly formatted citations + */ + public static boolean hasValidCitations(String text) { + if (text == null || text.isEmpty()) { + return true; // Empty text is valid + } + + // Check for basic citation patterns + return CITATION_PATTERN.matcher(text).find() || + FULL_CITATION_PATTERN.matcher(text).find() || + !containsUnformattedReferences(text); + } + + /** + * Checks if text contains unformatted references that should be citations + */ + private static boolean containsUnformattedReferences(String text) { + // Look for patterns that suggest unformatted references + String lowerText = text.toLowerCase(); + return lowerText.contains("journal article") || + lowerText.contains("report") || + (lowerText.contains("20") && lowerText.matches(".*\\b\\d{4}\\b.*")); + } +} \ No newline at end of file diff --git a/vspace/src/main/webapp/WEB-INF/views/layouts/main.html b/vspace/src/main/webapp/WEB-INF/views/layouts/main.html index 90e6a6d74..5a4ccf1c8 100644 --- a/vspace/src/main/webapp/WEB-INF/views/layouts/main.html +++ b/vspace/src/main/webapp/WEB-INF/views/layouts/main.html @@ -25,6 +25,7 @@ + diff --git a/vspace/src/main/webapp/WEB-INF/views/layouts/main_staff.html b/vspace/src/main/webapp/WEB-INF/views/layouts/main_staff.html index 7505d7521..2db782dea 100644 --- a/vspace/src/main/webapp/WEB-INF/views/layouts/main_staff.html +++ b/vspace/src/main/webapp/WEB-INF/views/layouts/main_staff.html @@ -26,6 +26,7 @@ + diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index d1708279e..e1b77885f 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -2900,9 +2900,29 @@
@@ -2961,9 +2981,29 @@ diff --git a/vspace/src/main/webapp/resources/extra/citations.css b/vspace/src/main/webapp/resources/extra/citations.css new file mode 100644 index 000000000..5c7467ccd --- /dev/null +++ b/vspace/src/main/webapp/resources/extra/citations.css @@ -0,0 +1,233 @@ +/* Citation Styling for Virtual Spaces */ + +/* In-text citation styling */ +.citation.in-text-citation { + color: #0066cc; + font-weight: 500; + background-color: rgba(0, 102, 204, 0.05); + padding: 1px 3px; + border-radius: 3px; + border-left: 2px solid #0066cc; + margin: 0 1px; + display: inline; +} + +/* Reference citation styling */ +.citation.reference-citation { + margin: 8px 0; + padding: 8px 12px; + background-color: #f8f9fa; + border-left: 4px solid #6c757d; + border-radius: 0 4px 4px 0; + font-size: 0.95em; + line-height: 1.4; + color: #495057; +} + +/* Citation help panel styling */ +.citation-help { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 15px; + margin-top: 10px; +} + +.citation-help h6 { + color: #495057; + margin-bottom: 10px; +} + +.citation-examples code { + background-color: #e9ecef; + color: #495057; + padding: 2px 6px; + border-radius: 3px; + font-size: 0.85em; + border: 1px solid #ced4da; + display: inline-block; + margin: 2px 0; +} + +.citation-examples p { + margin-bottom: 8px; + font-weight: 600; + color: #343a40; +} + +/* Styling for citation validation feedback */ +.citation-valid { + border-left-color: #28a745 !important; +} + +.citation-invalid { + border-left-color: #dc3545 !important; +} + +/* Text editor enhancement for citations */ +.EasyMDEContainer .citation-marker { + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 2px; + padding: 0 2px; +} + +/* Slide view citation styling */ +.textDiv .citation.in-text-citation { + font-size: inherit; + font-family: inherit; +} + +.textDiv .citation.reference-citation { + margin: 12px 0; + font-size: 0.9em; +} + +/* Hoverable Citation Help Tooltip */ +.citation-help-trigger { + position: relative; + display: inline-block; +} + +.citation-help-tooltip { + visibility: hidden; + opacity: 0; + position: absolute; + top: 30px; + left: -300px; + width: 320px; + background-color: #2c3e50; + color: #fff; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + z-index: 1050; + transition: opacity 0.2s, visibility 0.2s; + font-size: 12px; +} + +.citation-help-trigger:hover .citation-help-tooltip { + visibility: visible; + opacity: 1; +} + +.citation-help-content { + padding: 12px; + line-height: 1.3; +} + +.citation-help-content h6 { + margin-bottom: 8px; + color: #ecf0f1; + font-size: 13px; + font-weight: 600; +} + +.citation-help-content p { + margin-bottom: 8px; + color: #bdc3c7; + font-size: 11px; +} + +.citation-help-content code { + background-color: #34495e; + color: #e74c3c; + padding: 1px 4px; + border-radius: 2px; + font-family: 'Courier New', monospace; + font-size: 10px; +} + +.citation-help-content strong { + color: #ecf0f1; + font-weight: 600; +} + +/* Arrow pointing up to the icon */ +.citation-help-tooltip::before { + content: ''; + position: absolute; + top: -6px; + left: 310px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #2c3e50; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .citation-help-tooltip { + left: -250px; + width: 280px; + } + + .citation-help-tooltip::before { + left: 260px; + } +} + +/* Modal enhancements for citation guidance */ +.modal-lg .citation-help { + max-height: 400px; + overflow-y: auto; +} + +/* Responsive citation styling */ +@media (max-width: 768px) { + .citation.reference-citation { + margin: 6px 0; + padding: 6px 8px; + font-size: 0.85em; + } + + .citation-help { + padding: 10px; + } + + .citation-examples code { + font-size: 0.8em; + padding: 1px 4px; + } +} + +/* Print styles for citations */ +@media print { + .citation.in-text-citation { + background-color: transparent; + border: none; + color: #000; + padding: 0; + } + + .citation.reference-citation { + background-color: transparent; + border: none; + margin: 4px 0; + padding: 0; + color: #000; + } +} + +/* Exhibition module specific citation styling */ +.Group_8_Class .citation.in-text-citation { + font-size: inherit; + line-height: inherit; +} + +.Group_8_Class .citation.reference-citation { + margin: 10px 0; + width: 100%; + box-sizing: border-box; +} + +/* Citation formatting animation */ +.citation-format-animation { + animation: citationHighlight 0.5s ease-in-out; +} + +@keyframes citationHighlight { + 0% { background-color: #fff3cd; } + 50% { background-color: #ffeaa7; } + 100% { background-color: rgba(0, 102, 204, 0.05); } +} \ No newline at end of file diff --git a/vspace/src/test/java/edu/asu/diging/vspace/core/util/CitationFormatterTest.java b/vspace/src/test/java/edu/asu/diging/vspace/core/util/CitationFormatterTest.java new file mode 100644 index 000000000..3da872cac --- /dev/null +++ b/vspace/src/test/java/edu/asu/diging/vspace/core/util/CitationFormatterTest.java @@ -0,0 +1,116 @@ +package edu.asu.diging.vspace.core.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +/** + * Test class for CitationFormatter utility + */ +public class CitationFormatterTest { + + @Test + public void testFormatInTextCitation() { + String input = "This research shows [@Smith 2020] that citations work."; + String expected = "This research shows (Smith, 2020) that citations work."; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testFormatInTextCitationWithPages() { + String input = "As noted [@Jones 2019, pp. 15-20], proper formatting is important."; + String expected = "As noted (Jones, 2019, pp. 15-20), proper formatting is important."; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testFormatFullCitation() { + String input = "{Smith, J., 2020, Research Methods, Journal of Science, pp. 1-10}"; + String expected = "Smith, J. (2020). *Research Methods*. Journal of Science, pp. 1-10."; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testFormatMultipleInTextCitations() { + String input = "Research by [@Smith 2020] and [@Jones 2019] shows this."; + String expected = "Research by (Smith, 2020) and (Jones, 2019) shows this."; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testMixedCitations() { + String input = "This study [@Chen 2025] examines the issue.\n\n" + + "{Chen, A., 2025, Code Comprehension in Scientific Programming, arXiv, http://arxiv.org/abs/2501.10037}"; + String result = CitationFormatter.formatCitations(input); + + assertTrue("Should contain formatted in-text citation", result.contains("(Chen, 2025)")); + assertTrue("Should contain formatted reference", result.contains("Chen, A. (2025)")); + assertTrue("Should contain italicized title", result.contains("*Code Comprehension in Scientific Programming*")); + } + + @Test + public void testAuthorNameFormatting() { + String input = "[@John Smith 2020]"; + String expected = "(Smith, J., 2020)"; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testEtAlCitation() { + String input = "[@Smith et al. 2020]"; + String expected = "(Smith et al., 2020)"; + String result = CitationFormatter.formatCitations(input); + assertEquals(expected, result); + } + + @Test + public void testValidCitationsDetection() { + String validText1 = "This has [@Smith 2020] proper citations."; + String validText2 = "Reference: {Smith, J., 2020, Title, Journal}"; + String validText3 = "No citations but valid text."; + String invalidText = "This has 2020 journal article but no proper format."; + + assertTrue("Should recognize valid in-text citation", + CitationFormatter.hasValidCitations(validText1)); + assertTrue("Should recognize valid full citation", + CitationFormatter.hasValidCitations(validText2)); + assertTrue("Should accept text without citations", + CitationFormatter.hasValidCitations(validText3)); + assertFalse("Should detect improperly formatted references", + CitationFormatter.hasValidCitations(invalidText)); + } + + @Test + public void testEmptyAndNullInput() { + assertEquals("Should handle null input", null, CitationFormatter.formatCitations(null)); + assertEquals("Should handle empty input", "", CitationFormatter.formatCitations("")); + assertTrue("Should validate null as valid", CitationFormatter.hasValidCitations(null)); + assertTrue("Should validate empty as valid", CitationFormatter.hasValidCitations("")); + } + + @Test + public void testComplexReference() { + String input = "{Chen, Alyssa, 2025, Exploring Code Comprehension in Scientific Programming: Preliminary Insights from Research Scientists, arXiv, http://arxiv.org/abs/2501.10037}"; + String result = CitationFormatter.formatCitations(input); + + assertTrue("Should format author correctly", result.contains("Chen, A.")); + assertTrue("Should include year", result.contains("(2025)")); + assertTrue("Should italicize title", result.contains("*Exploring Code Comprehension")); + assertTrue("Should include journal", result.contains("arXiv")); + assertTrue("Should include URL", result.contains("http://arxiv.org/abs/2501.10037")); + } + + @Test + public void testPreserveNonCitationText() { + String input = "Regular text with no citations should remain unchanged."; + String result = CitationFormatter.formatCitations(input); + assertEquals("Non-citation text should be preserved", input, result); + } +} \ No newline at end of file