diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Pages/About.cshtml | 15 | ||||
| -rw-r--r-- | src/Pages/About.cshtml.cs | 10 | ||||
| -rw-r--r-- | src/Pages/Index.cshtml | 105 | ||||
| -rw-r--r-- | src/Pages/Index.cshtml.cs | 34 | ||||
| -rw-r--r-- | src/Pages/Read.cshtml | 50 | ||||
| -rw-r--r-- | src/Pages/Read.cshtml.cs | 24 | ||||
| -rw-r--r-- | src/Pages/Shared/_Layout.cshtml | 20 | ||||
| -rw-r--r-- | src/Services/GrabberService.cs | 107 | ||||
| -rw-r--r-- | src/Utilities/HtmlSanitiser.cs | 29 | ||||
| -rw-r--r-- | src/wwwroot/index.css | 89 |
10 files changed, 301 insertions, 182 deletions
diff --git a/src/Pages/About.cshtml b/src/Pages/About.cshtml new file mode 100644 index 0000000..766c2a4 --- /dev/null +++ b/src/Pages/About.cshtml @@ -0,0 +1,15 @@ +@page "/om" +@model I2R.LightNews.Pages.About + +@{ + ViewData["Title"] = "Om"; +} + +<p>Lettnytt viser deg forenklet versjon av norske nettaviser.</p> +<p>Noen artikler kan også leses forenklet.</p> +<p>Her er en ikke-komplett liste over inngrep:</p> +<ul> + <li>Fjerner bilder, videoer og svakt relevante lenker (reklame) til andre artikler.</li> + <li>For nrk.no: Fjerner lenker til /mat, /radio og /tv</li> +</ul> +<p>E-postadresse: lettnytt@oiee.no</p>
\ No newline at end of file diff --git a/src/Pages/About.cshtml.cs b/src/Pages/About.cshtml.cs new file mode 100644 index 0000000..b30cd51 --- /dev/null +++ b/src/Pages/About.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace I2R.LightNews.Pages; + +public class About : PageModel +{ + public void OnGet() { + + } +}
\ No newline at end of file diff --git a/src/Pages/Index.cshtml b/src/Pages/Index.cshtml index f01b263..0501e3e 100644 --- a/src/Pages/Index.cshtml +++ b/src/Pages/Index.cshtml @@ -1,21 +1,94 @@ @page "{site?}" @model IndexModel @{ - ViewData["Title"] = Model.Source.Name; + ViewData["Title"] = Model.PageTitle; } +@if (Model.FrontPage != default) { + foreach (var article in Model.FrontPage.Articles) { + <section class="news-link"> + <a href="/@Model.FrontPage.Name?url=@article.Href"> + <h2>@Html.Raw(article.Title)</h2> + </a> + <div class="bar"> + <div class="from-the-left"> + <button class="reset link" onclick="hide_article(this)">Gjem</button> | + </div> + <div class="from-the-right"> + <a href="@article.Href" class="source-link" rel="noreferrer">Les på nrk.no</a> + </div> + </div> + </section> + } + <footer> + <p> + <small> + @Model.FrontPage.Attribution © @Model.FrontPage.Name, @(DateTime.UtcNow.Subtract(Model.FrontPage.Created).Minutes) minutter siden + </small> + </p> + </footer> + @section scripts { + <script> + const ignoreSessionStorageKey = "frontpage_ignores"; + function hide_article(el) { + const linkEl = el.closest(".news-link"); + const ignoreLink = new URL(linkEl.querySelector("a").href).searchParams.get("url"); + const currentIgnores = sessionStorage.getItem(ignoreSessionStorageKey); + let newIgnores = []; + if (currentIgnores) newIgnores = JSON.parse(currentIgnores); + newIgnores.push(ignoreLink) + sessionStorage.setItem(ignoreSessionStorageKey, JSON.stringify(newIgnores)); + linkEl.remove() + } + + const ignores = sessionStorage.getItem(ignoreSessionStorageKey) + if (ignores) { + for (const ignore of JSON.parse(ignores)) { + console.log(ignore) + document.querySelector("a[href*='"+ignore+"']").closest(".news-link").remove(); + } + } + </script> + } + } else if (Model.Article != default) { + <div id="art-header" style="display: flex; justify-content: space-between"> + <div> + <h1>@Model.Article.Title</h1> + <p>@Model.Article.Subtitle</p> + </div> + </div> -@foreach (var article in Model.Source.Articles) { - <section class="news-link"> - <a href="/les/@Model.Source.Name?url=@article.Href"> - <h2>@Html.Raw(article.Title)</h2> - </a> - <a href="@article.Href" class="source-link" rel="noreferrer">Les på nrk.no</a> - </section> -} -<footer> - <p> - <small> - @Model.Source.Attribution © @Model.Source.Name, @(DateTime.UtcNow.Subtract(Model.Source.Created).Minutes) minutter siden - </small> - </p> -</footer>
\ No newline at end of file + <article id="art-body"> + @Html.Raw(Model.Article.Content) + </article> + + <footer> + <div style="flex-direction:column"> + @foreach (var author in Model.Article.Authors) { + <small style="white-space: nowrap"><b>@author.Name</b>: @author.Title</small> + <br/> + } + </div> + <div style="flex-direction: column"> + @if (Model.Article.PublishedAt != default) { + <small style="white-space: nowrap">Publisert: @Model.Article.PublishedAt.ToString("dd-MM-yyyy hh:mm:ss")</small> + } + @if (Model.Article.UpdatedAt != default) { + <br/> + <small style="white-space: nowrap">Oppdatert: @Model.Article.UpdatedAt.ToString("dd-MM-yyyy hh:mm:ss")</small> + } + <br/> + <small> + <a href="@Model.Article.Href" no-interception>Les på nrk.no</a> + </small> + </div> + </footer> + +@section scripts { + <script> + document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll("a:not([no-interception])").forEach(el => { + if (el.href.indexOf("nrk.no") !== -1) el.href = "/nrk?url=" + el.href; + }); + }) + </script> +}}
\ No newline at end of file diff --git a/src/Pages/Index.cshtml.cs b/src/Pages/Index.cshtml.cs index 80b0ed0..666c75e 100644 --- a/src/Pages/Index.cshtml.cs +++ b/src/Pages/Index.cshtml.cs @@ -13,16 +13,36 @@ public class IndexModel : PageModel _grabber = grabber; } - public NewsSource Source { get; set; } + public NewsSource FrontPage { get; set; } + public NewsArticle Article { get; set; } + public string PageTitle { get; set; } - public async Task<ActionResult> OnGet(string site) { - Source = site switch { - "nrk" => await _grabber.GrabNrkAsync(), - _ => default + public async Task<ActionResult> OnGet([FromRoute] string site, [FromQuery] string url = default) { + PageTitle = site switch { + "nrk" => "NRK", + _ => "" }; - if (Source == default) { - return Redirect("/nrk"); + if (url.IsNullOrWhiteSpace()) { + FrontPage = site switch { + "nrk" => await _grabber.GrabNrkAsync(), + _ => default + }; + + if (FrontPage == default) { + return Redirect("/nrk"); + } + } else { + Article = site switch { + "nrk" => await _grabber.GrabNrkArticleAsync(url), + _ => default + }; + + if (Article == default) { + return Redirect(url); + } + + PageTitle = PageTitle + " - " + Article.Title; } return Page(); diff --git a/src/Pages/Read.cshtml b/src/Pages/Read.cshtml deleted file mode 100644 index 9cff853..0000000 --- a/src/Pages/Read.cshtml +++ /dev/null @@ -1,50 +0,0 @@ -@page "/les/{site}" -@model ReadModel -@{ - ViewData["Title"] = Model.Source.Title; -} - -<div id="art-header" style="display: flex; justify-content: space-between"> - <div> - <h1>@Model.Source.Title</h1> - <p>@Model.Source.Subtitle</p> - </div> -</div> - -<article id="art-body"> - @Html.Raw(Model.Source.Content) -</article> - -<footer> - <p> - <div style="display: flex; flex-direction: column; flex-wrap: nowrap;"> - <div style="flex-direction:column"> - @foreach (var author in Model.Source.Authors) { - <small style="white-space: nowrap"><b>@author.Name</b>: @author.Title</small> - <br/> - } - </div> - <div style="flex-direction: column"> - @if (Model.Source.PublishedAt != default) { - <small style="white-space: nowrap">Publisert: @Model.Source.PublishedAt.ToString("dd-MM-yyyy hh:mm:ss")</small> - } - @if (Model.Source.UpdatedAt != default) { - <br/> - <small style="white-space: nowrap">Oppdatert: @Model.Source.UpdatedAt.ToString("dd-MM-yyyy hh:mm:ss")</small> - } - <br/> - <small> - <a href="@Model.Source.Href" no-interception>Les på nrk.no</a> - </small> - </div> - </div> - </p> -</footer> - -<script> -document.addEventListener("DOMContentLoaded", () => { - document.querySelectorAll("a:not([no-interception])").forEach(el => { - if (el.href.indexOf("nrk.no") !== -1) el.href = "/les/nrk?url=" + el.href; - }); -}) -</script>
\ No newline at end of file diff --git a/src/Pages/Read.cshtml.cs b/src/Pages/Read.cshtml.cs deleted file mode 100644 index 1a13ec0..0000000 --- a/src/Pages/Read.cshtml.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace I2R.LightNews.Pages; - -public class ReadModel : PageModel -{ - private readonly GrabberService _grabber; - - public NewsArticle Source { get; set; } - - public ReadModel(GrabberService grabber) { - _grabber = grabber; - } - - public async Task<ActionResult> OnGet([FromRoute] string site, [FromQuery] string url) { - Source = site switch { - "nrk" => await _grabber.GrabNrkArticleAsync(url), - _ => default - }; - if (Source == default) return Redirect(url); - return Page(); - } -}
\ No newline at end of file diff --git a/src/Pages/Shared/_Layout.cshtml b/src/Pages/Shared/_Layout.cshtml index 09babeb..c3ca817 100644 --- a/src/Pages/Shared/_Layout.cshtml +++ b/src/Pages/Shared/_Layout.cshtml @@ -4,17 +4,29 @@ <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="stylesheet" href="/index.css"> + @await RenderSectionAsync("head", false) <title>@ViewData["Title"] - Lettnytt</title> </head> <body> -<header> - <nav style="display: flex; flex-direction: row; gap: 0 15px"> - <a href="/nrk" style="color: @(Path.StartsWith("/nrk") ? "black" : "blue")">NRK</a> - <a href="/dagbladet" style="color: @(Path.StartsWith("/dagbladet") ? "black" : "blue")">Dagbladet</a> +<header id="top-bar"> + <nav> + <div class="left"> + <a href="/nrk" class="@(Context.Request.Path.StartsWithSegments("/nrk") ? "active" : "")">NRK</a> + <a href="/dagbladet" class="@(Context.Request.Path.StartsWithSegments("/dagbladet") ? "active" : "")">Dagbladet</a> + <a href="/aftenposten" class="@(Context.Request.Path.StartsWithSegments("/aftenposten") ? "active" : "")">Aftenposten</a> + <a href="/vg" class="@(Context.Request.Path.StartsWithSegments("/vg") ? "active" : "")">VG</a> + <a href="/dn" class="@(Context.Request.Path.StartsWithSegments("/dn") ? "active" : "")">DN</a> + <a href="/e24" class="@(Context.Request.Path.StartsWithSegments("/e24") ? "active" : "")">E24</a> + <a href="/kode24" class="@(Context.Request.Path.StartsWithSegments("/kode24") ? "active" : "")">Kode 24</a> + </div> + <div class="right"> + <a href="/om" class="@(Context.Request.Path.StartsWithSegments("/om") ? "active" : "")">Om lettnytt</a> + </div> </nav> </header> <main> @RenderBody() </main> +@await RenderSectionAsync("scripts", false) </body> </html>
\ No newline at end of file diff --git a/src/Services/GrabberService.cs b/src/Services/GrabberService.cs index 4886023..d6650a2 100644 --- a/src/Services/GrabberService.cs +++ b/src/Services/GrabberService.cs @@ -33,7 +33,8 @@ public class GrabberService "nrk.no/mat", "nrk.no/radio", "nrk.no/tv", - "nrk.no/xl" + "nrk.no/video", + "nrk.no/podkast" }; return strippedUrl.StartsWith("nrk.no") && ignored.All(c => !strippedUrl.Contains(c)); @@ -44,59 +45,69 @@ public class GrabberService using var md5 = MD5.Create(); var articleFilePrefix = "art-" + NrkPrefix + "-" + Convert.ToHexString(md5.ComputeHash(Encoding.UTF8.GetBytes(url))); return await _memoryCache.GetOrCreateAsync(articleFilePrefix, async entry => { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); - var source = await GrabSourceAsync(url, articleFilePrefix); - var parser = new HtmlParser(); - var doc = await parser.ParseDocumentAsync(source.Content); - var result = new NewsArticle() { - CachedAt = source.CacheFileCreatedAt, - Href = url, - Title = doc.QuerySelector("h1.title")?.TextContent, - Subtitle = doc.QuerySelector(".article-lead p")?.TextContent, - Authors = new List<NewsArticle.Author>() + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + var source = await GrabSourceAsync(url, articleFilePrefix); + var parser = new HtmlParser(); + var doc = await parser.ParseDocumentAsync(source.Content); + var result = new NewsArticle() { + CachedAt = source.CacheFileCreatedAt, + Href = url, + Title = doc.QuerySelector("h1.title")?.TextContent, + Subtitle = doc.QuerySelector(".article-lead p")?.TextContent, + Authors = new List<NewsArticle.Author>() + }; + + foreach (var authorNode in doc.QuerySelectorAll(".authors .author")) { + var author = new NewsArticle.Author() { + Name = authorNode.QuerySelector(".author__name")?.TextContent, + Title = authorNode.QuerySelector(".author__role")?.TextContent }; + result.Authors.Add(author); + } - foreach (var authorNode in doc.QuerySelectorAll(".authors .author")) { - var author = new NewsArticle.Author() { - Name = authorNode.QuerySelector(".author__name")?.TextContent, - Title = authorNode.QuerySelector(".author__role")?.TextContent - }; - result.Authors.Add(author); - } + DateTime.TryParse(doc.QuerySelector("time.datePublished")?.Attributes["datetime"]?.Value, out var published); + DateTime.TryParse(doc.QuerySelector("time.dateModified")?.Attributes["datetime"]?.Value, out var modified); - DateTime.TryParse(doc.QuerySelector("time.datePublished")?.Attributes["datetime"]?.Value, out var published); - DateTime.TryParse(doc.QuerySelector("time.dateModified")?.Attributes["datetime"]?.Value, out var modified); + result.UpdatedAt = modified; + result.PublishedAt = published; - result.UpdatedAt = modified; - result.PublishedAt = published; + var defaultExcludes = new List<string>() { + ".dhks-background", + ".dhks-actions", + ".dhks-credits", + ".dhks-sticky-reset", + ".dhks-byline", + ".compilation-reference", + ".section-reference", + ".image", + ".fact__expand", + ".image-reference", + ".video-reference", + ".article-body--updating", + ".external-reference", + ".reference", + ".atlas-reference", + ".remoterenderedcontent-reference", + "text:Følg utviklingen i NRKs Nyhetssenter", + "text:Bli med i debatten under" + }; - if (doc.QuerySelector("kortstokk-app") != default) { - var excludes = new List<string>() { - ".dhks-background", - ".dhks-actions", - ".dhks-credits", - ".dhks-sticky-reset", - ".dhks-byline" - }; - result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".dhks-cardSection").InnerHtml, string.Join(',', excludes)); - } else if (url.Contains("nrk.no/nyheter")) { - result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".bulletin-text").InnerHtml); - } else { - var excludes = new List<string>() { - ".compilation-reference", - ".section-reference", - ".widget", - ".image-reference", - ".video-reference", - ".article-body--updating", - ".reference" - }; - result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".article-body").InnerHtml, string.Join(',', excludes)); - } + if (doc.QuerySelector("kortstokk-app") != default) { + result.Title = doc.QuerySelector(".dhks-title span")?.TextContent; + result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".dhks-cardSection").InnerHtml, string.Join(',', defaultExcludes)); + } else if (url.Contains("/xl/")) { + var subtitle = doc.QuerySelector(".article-feature__intro p").InnerHtml; + result.Title = doc.QuerySelector(".article-feature__intro h1").TextContent; + var contentHtml = doc.QuerySelector(".article-feature__body").InnerHtml; + result.Content = HtmlSanitiser.SanitizeHtmlFragment(subtitle + contentHtml, string.Join(',', defaultExcludes)); + } else if (url.Contains("nrk.no/nyheter") || doc.QuerySelector(".bulletin-text") != default) { + result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".bulletin-text").InnerHtml); + } else { + result.Content = HtmlSanitiser.SanitizeHtmlFragment(doc.QuerySelector(".article-body").InnerHtml, string.Join(',', defaultExcludes)); + } - return result; - }) - ; + return result; + }); } public async Task<NewsSource> GrabNrkAsync() { diff --git a/src/Utilities/HtmlSanitiser.cs b/src/Utilities/HtmlSanitiser.cs index 645b09d..9fbc003 100644 --- a/src/Utilities/HtmlSanitiser.cs +++ b/src/Utilities/HtmlSanitiser.cs @@ -25,7 +25,6 @@ public static class HtmlSanitiser Sanitize(element.ChildNodes[i], excludeSelectors); } - return element.InnerHtml; } @@ -37,30 +36,26 @@ public static class HtmlSanitiser return element; } - private static void Sanitize(INode node, string excludeSelectors = default) { + private static void Sanitize(INode node, string excludeSelectors = default, string excludeText = default) { if (node is IElement htmlElement) { if (excludeSelectors.HasValue()) { foreach (var selector in excludeSelectors.Split(',')) { - if (selector.StartsWith(".")) { - if (htmlElement.ClassList.Contains(selector.Replace(".", ""))) { - Console.WriteLine("Removed: " + htmlElement.TagName + ", because of: " + selector); - htmlElement.Remove(); - continue; - } + if ( + selector.StartsWith(".") && htmlElement.ClassList.Contains(selector.Replace(".", "")) + || selector.StartsWith("#") && htmlElement.Id == selector.Replace("#", "") + || htmlElement.TagName == selector.ToUpper() + || selector.StartsWith("text:") && htmlElement.TextContent.Contains(selector.Replace("text:", "")) + ) { + Console.WriteLine("Removed: " + htmlElement.TagName + ", because of: " + selector); + htmlElement.Remove(); } - if (selector.StartsWith("#")) { - if (htmlElement.Id == selector.Replace("#", "")) { - Console.WriteLine("Removed: " + htmlElement.TagName + ", because of: " + selector); + if (!selector.StartsWith("text:")) { + foreach (var element in htmlElement.QuerySelectorAll(selector)) { + Console.WriteLine("Removed: " + element.TagName + ", because of: " + selector); htmlElement.Remove(); - continue; } } - - if (htmlElement.TagName == selector.ToUpper()) { - Console.WriteLine("Removed: " + htmlElement.TagName + ", because of: " + selector); - htmlElement.Remove(); - } } } diff --git a/src/wwwroot/index.css b/src/wwwroot/index.css index 2c5ac3f..b1cc611 100644 --- a/src/wwwroot/index.css +++ b/src/wwwroot/index.css @@ -71,7 +71,7 @@ select { body { min-height: 100vh; text-rendering: optimizeSpeed; - line-height: 1.5; + line-height: 150%; display: flex; flex-direction: column; padding: 5vh clamp(1rem, 5vw, 3rem) 1rem; @@ -95,13 +95,9 @@ main { footer { margin-top: auto; padding-top: var(--layout-spacing); -} - -footer p { - border-top: 1px solid #ccc; - padding-top: 0.25em; - font-size: 0.9rem; - color: #767676; + display: flex; + flex-direction: column; + flex-wrap: nowrap; } :is(h1, h2, h3) { @@ -117,27 +113,88 @@ article * + * { } .quote-text { - padding: 5px 15px 0 0; + padding: 0 25px; font-style: italic; - background: #e9e9e9; } .quote-source { - padding-left: 20px; + padding-left: 50px; +} + +.reset { + all: unset; } .news-link { - margin-bottom: 8px; + margin-bottom: 1vw; display: flex; - flex-direction: column + flex-direction: column; + border-bottom: 1px solid dimgray; } .news-link h2 { font-size: 18px; } +.news-link .bar { + font-size: 14px !important; + display: flex; + justify-content: space-between; +} + +.news-link .bar .from-the-right { + justify-content: end; + display: flex; +} + .news-link .source-link { - font-size: 14px; + width: fit-content; +} + +#top-bar nav { + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; +} + +#top-bar nav .left { display: flex; - justify-content: end -}
\ No newline at end of file + gap: 0 15px; + flex-direction: row +} + +#top-bar a { + color: blue; +} + +#top-bar a.active { + font-weight: 600; + color: black; +} + +a, +.link { + color: blue; + text-decoration: none; + cursor: pointer; +} + +a:visited { + color: blueviolet; +} + +@media (prefers-color-scheme: dark) { + body { + color: #eee; + background: #151515; + } + + a, .link { + color: #4d4dff !important; + } + + a:visited { + color: #ad6beb !important; + } +} |
