aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Pages/About.cshtml15
-rw-r--r--src/Pages/About.cshtml.cs10
-rw-r--r--src/Pages/Index.cshtml105
-rw-r--r--src/Pages/Index.cshtml.cs34
-rw-r--r--src/Pages/Read.cshtml50
-rw-r--r--src/Pages/Read.cshtml.cs24
-rw-r--r--src/Pages/Shared/_Layout.cshtml20
-rw-r--r--src/Services/GrabberService.cs107
-rw-r--r--src/Utilities/HtmlSanitiser.cs29
-rw-r--r--src/wwwroot/index.css89
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 &copy; @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 &copy; @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;
+ }
+}