diff --git a/src/NexusReader.Infrastructure/Services/BookStorageService.cs b/src/NexusReader.Infrastructure/Services/BookStorageService.cs
index 9af4ee5..d1f844f 100644
--- a/src/NexusReader.Infrastructure/Services/BookStorageService.cs
+++ b/src/NexusReader.Infrastructure/Services/BookStorageService.cs
@@ -27,6 +27,7 @@ public class BookStorageService : IBookStorageService
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads");
EnsureDirectoryExists(uploadsFolder);
+ fileName = SanitizeFileName(fileName);
var uniqueFileName = $"{Guid.NewGuid()}_{fileName}";
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
@@ -52,6 +53,7 @@ public class BookStorageService : IBookStorageService
var coversFolder = Path.Combine(_environment.WebRootPath, "covers");
EnsureDirectoryExists(coversFolder);
+ fileName = SanitizeFileName(fileName);
var uniqueFileName = $"{Guid.NewGuid()}_{fileName}";
var filePath = Path.Combine(coversFolder, uniqueFileName);
@@ -63,6 +65,25 @@ public class BookStorageService : IBookStorageService
return $"covers/{uniqueFileName}";
}
+ private string SanitizeFileName(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName)) return fileName;
+
+ var sanitized = fileName
+ .Replace('\u00A0', ' ')
+ .Replace('\u2007', ' ')
+ .Replace('\u200B', ' ')
+ .Replace('\u202F', ' ');
+
+ var invalidChars = Path.GetInvalidFileNameChars();
+ foreach (var c in invalidChars)
+ {
+ sanitized = sanitized.Replace(c, '_');
+ }
+
+ return sanitized;
+ }
+
private void EnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
diff --git a/src/NexusReader.Infrastructure/Services/EpubReaderService.cs b/src/NexusReader.Infrastructure/Services/EpubReaderService.cs
index 6079d42..4154640 100644
--- a/src/NexusReader.Infrastructure/Services/EpubReaderService.cs
+++ b/src/NexusReader.Infrastructure/Services/EpubReaderService.cs
@@ -18,7 +18,7 @@ public class EpubReaderService : IEpubReader
private readonly ILogger[^>]*?\bsrc=[""'])(?
]*?\bsrc=[""'])(?
]*>|]*>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex StyleScriptRegex = new(@"<(style|script)\b[^>]*>.*?\1>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
@@ -27,6 +27,9 @@ public class EpubReaderService : IEpubReader
private static readonly Regex ImgTagSanitizerRegex = new(@"
]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SrcAttributeRegex = new(@"\bsrc=[""'](?
";
+ }
+
+ var hrefMatch = HrefAttributeRegex.Match(attrs);
+ if (hrefMatch.Success)
+ {
+ var hrefVal = hrefMatch.Groups["href"].Value;
+ var cleanedAttrs = HrefAttributeRegex.Replace(attrs, "");
+ return $"
";
+ }
+
+ return match.Value;
+ });
+ }
+
private static string ResolveRelativePath(string basePath, string relativePath)
{
if (string.IsNullOrEmpty(relativePath)) return string.Empty;
diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css
index 604e35c..4456cdb 100644
--- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css
+++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css
@@ -45,7 +45,7 @@
min-height: calc(100vh - 180px);
display: flex;
flex-direction: column;
- gap: 1.5rem;
+ gap: 0.75rem;
position: relative;
padding: 3rem 4rem 15rem 4rem;
/* Large padding-bottom for reachability, plus comfortable side margins */
@@ -69,10 +69,21 @@
.block-wrapper {
transition: all 0.5s ease;
border-radius: 8px;
- padding: 8px;
+ padding: 2px 8px;
border: 1px solid transparent;
}
+/* Pull subsequent block closer to headings or bold exercise labels */
+.block-wrapper:has(h1),
+.block-wrapper:has(h2),
+.block-wrapper:has(h3),
+.block-wrapper:has(h4),
+.block-wrapper:has(h5),
+.block-wrapper:has(h6),
+.block-wrapper:has(p > strong) {
+ margin-bottom: -0.25rem;
+}
+
/* Typographic refinement for TextSegmentBlock */
::deep .nexus-ebook {
font-family: 'Merriweather', serif !important;
@@ -90,12 +101,24 @@
/* Warm charcoal for legibility */
}
+/* Reset default margins for elements within separate block-wrappers */
+::deep .nexus-ebook p,
+::deep .nexus-ebook h1,
+::deep .nexus-ebook h2,
+::deep .nexus-ebook h3,
+::deep .nexus-ebook h4,
+::deep .nexus-ebook h5,
+::deep .nexus-ebook h6 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
/* Callout Box styling for legacy blockquote segments */
::deep .nexus-ebook blockquote {
background-color: rgba(255, 255, 255, 0.02);
border-left: 4px solid var(--nexus-neon);
padding: 1rem 1.25rem;
- margin: 1.5rem 0 1.5rem 0;
+ margin: 1rem 0 1rem 0;
border-radius: 0 8px 8px 0;
font-size: 1.05rem;
color: #e2e8f0;
@@ -116,7 +139,7 @@
color: #e0e0e0;
padding: 1.25rem;
border-radius: 8px;
- margin: 2rem 0;
+ margin: 1.25rem 0;
overflow-x: auto;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid var(--nexus-neon);
@@ -353,7 +376,7 @@
padding-left: 18px !important;
padding-right: 18px !important;
padding-bottom: 4rem;
- /* Safe breathing room */
+ gap: 0.75rem !important; /* Tighter spacing on mobile */
}
}
@@ -367,8 +390,8 @@
::deep .nexus-ebook h1 {
font-size: 1.35rem !important;
line-height: 1.4 !important;
- margin-top: 1.5rem !important;
- margin-bottom: 1rem !important;
+ margin-top: 0.5rem !important; /* Tighter margins on mobile */
+ margin-bottom: 0.25rem !important;
}
}
@@ -580,4 +603,44 @@
transform: rotate(0deg) scale(1);
opacity: 1;
}
+}
+
+/* Ebook Image Scaling, Alignment, and Separation Lines */
+.block-wrapper:has(img) {
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ padding: 1rem 0;
+ margin: 0.5rem 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+}
+
+.theme-light .block-wrapper:has(img) {
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+::deep .nexus-ebook img {
+ width: 100%;
+ max-width: 100%;
+ max-height: 75vh;
+ height: auto;
+ display: block;
+ border-radius: 8px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ opacity: 0.9;
+ transition: opacity 0.25s ease;
+}
+
+::deep .nexus-ebook img:hover {
+ opacity: 1;
+}
+
+.theme-light ::deep .nexus-ebook img {
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ opacity: 1;
}
\ No newline at end of file
diff --git a/src/NexusReader.Web/Program.cs b/src/NexusReader.Web/Program.cs
index d3484d3..08d30bd 100644
--- a/src/NexusReader.Web/Program.cs
+++ b/src/NexusReader.Web/Program.cs
@@ -1,4 +1,5 @@
using NexusReader.Web.Components;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Components;
using NexusReader.Application;
@@ -121,6 +122,17 @@ builder.Services.AddAuthentication(options =>
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
+ .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ options.Authority = builder.Configuration["Jwt:Authority"] ?? "https://example.com/";
+ options.Audience = builder.Configuration["Jwt:Audience"] ?? "NexusReaderAPI";
+ options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true
+ };
+ })
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"] ?? "placeholder-id";
diff --git a/tests/NexusReader.Application.Tests/Services/EpubReaderServiceTests.cs b/tests/NexusReader.Application.Tests/Services/EpubReaderServiceTests.cs
index 81609bf..8c960ca 100644
--- a/tests/NexusReader.Application.Tests/Services/EpubReaderServiceTests.cs
+++ b/tests/NexusReader.Application.Tests/Services/EpubReaderServiceTests.cs
@@ -267,6 +267,74 @@ public class EpubReaderServiceTests : IDisposable
colonResult.Errors.First().Message.Should().Contain("Invalid resource path");
}
+ [Fact]
+ public void RewriteImageUrls_PreservesImgPrefix()
+ {
+ // Arrange
+ var method = typeof(EpubReaderService).GetMethod("RewriteImageUrls", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ method.Should().NotBeNull();
+
+ var input = "
";
+ var ebookId = Guid.NewGuid();
+
+ // Act
+ var result = (string)method.Invoke(null, new object[] { input, ebookId, "OEBPS/cover-page.xhtml" });
+
+ // Assert
+ result.Should().StartWith("";
+ var inputHref = "
")] + [InlineData("