Switch an item between its HTML and its Markdown (#285)
A feed that sends <source:markdown> beside its HTML (Scripting News) has its Markdown kept in entries.markdown, added to existing databases by an ALTER. A stored item gets it when a scan finds it told differently, the Markdown counting as a change, the way corrections arrive (#141). The API sends it as written and made HTML, cleaned by ammonia like the description. The item's page has a choice between its meta line and its buttons: HTML, Markdown or Markdown source, remembered in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- An item sent as both HTML and Markdown, as Scripting News's are, has a choice beside its buttons to show it as the publisher's HTML, as the Markdown, or as the Markdown source, kept from one item to the next (#285).
|
||||
- A show inside an OPML subscription has a Keep button, a plus, on its page, so it stays on your list as a show of its own if the OPML drops it or you unsubscribe from the OPML (#280).
|
||||
- Admin, Server: a setting to give everyone Signals on the server's TypeSafe key (#271).
|
||||
- Admin, Server: how many days back a new Signal reads (#276).
|
||||
|
||||
@@ -848,6 +848,8 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i,
|
||||
/* The reader's buttons go as one: as items of the line itself, a long feed name took them onto
|
||||
the next line one at a time (#146). */
|
||||
.dacts{display:flex;gap:inherit;flex:none}
|
||||
.dmeta .tview{width:auto;padding:3px 6px;font-size:12px}
|
||||
.dbody .mdsrc{white-space:pre-wrap;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||
#detail .encbox{margin:0 0 10px}
|
||||
.dbody{font-size:14.5px;line-height:1.65;overflow-wrap:anywhere;color:var(--fg)}
|
||||
.dbody img{max-width:100%;height:auto;border-radius:6px}
|
||||
|
||||
@@ -376,6 +376,12 @@ function showDetail(e: Entry|null){
|
||||
box.innerHTML=detailHtml(e);
|
||||
if(files) files.innerHTML=narrow?'':`<div class="fhd">Files</div>${encs}`;
|
||||
$<HTMLButtonElement>('#dback').onclick=()=>showDetail(null);
|
||||
const view=box.querySelector<HTMLSelectElement>('.tview');
|
||||
if(view) view.onchange=()=>{
|
||||
textView=view.value;
|
||||
try{ localStorage.setItem('ipx.textview',textView); }catch{}
|
||||
box.querySelector('.dbody')!.innerHTML=textOf(e);
|
||||
};
|
||||
for(const root of [box,files]) if(root) $$('button[data-a]',root).forEach(b=>
|
||||
b.onclick=()=>epAction(b.dataset.a!,e,null,b.dataset.enc?Number(b.dataset.enc):undefined));
|
||||
syncPlayButtons();
|
||||
@@ -401,6 +407,8 @@ function detailHtml(e: Entry){
|
||||
[f&&esc(f.title||f.id), num, dateOf(e.published), e.duration&&clock(e.duration),
|
||||
e.enclosures.some(x=>x.deadair_pending)&&DEAD_PENDING]
|
||||
.filter(Boolean).map(s=>`<span>${s}</span>`).join('<span class="dot"></span>')}
|
||||
${e.markdown?`<select class="tview" aria-label="Show the text as">${TEXT_VIEWS.map(([v,label])=>
|
||||
`<option value="${v}"${v===textView?' selected':''}>${label}</option>`).join('')}</select>`:''}
|
||||
<span class="dacts"><button class="btn ico" data-a="read" title="Mark ${e.read?'unread':'read'}"
|
||||
aria-label="Mark ${e.read?'unread':'read'}" aria-pressed="${!!e.read}">${e.read?ICON.check:ICON.unread}</button>
|
||||
<button class="btn ico" data-a="flag" title="${e.flagged?'Pinned: never deleted. Unpin':'Pin, so it is never deleted'}"
|
||||
@@ -410,7 +418,18 @@ function detailHtml(e: Entry){
|
||||
<button class="btn ico" data-a="share" title="Share" aria-label="Share">${ICON.share}</button>`:''}</span>
|
||||
</div>
|
||||
${narrow?encs:''}
|
||||
<div class="dbody">${(e.description&&e.description.trim())||'<em>No show notes.</em>'}</div>`;
|
||||
<div class="dbody">${textOf(e)}</div>`;
|
||||
}
|
||||
|
||||
/// How an item whose feed sends Markdown beside its HTML is shown, kept from one item to the
|
||||
/// next (#285): the publisher's HTML, the Markdown made HTML, or the Markdown as written.
|
||||
const TEXT_VIEWS=[['html','HTML'],['markdown','Markdown'],['source','Markdown source']];
|
||||
let textView=(()=>{ try{ return localStorage.getItem('ipx.textview'); }catch{ return null; } })()||'html';
|
||||
function textOf(e: Entry){
|
||||
// Both cleaned server-side with ammonia, as the description is.
|
||||
if(e.markdown&&textView==='markdown'&&e.markdown_html) return e.markdown_html;
|
||||
if(e.markdown&&textView==='source') return `<pre class="mdsrc">${esc(e.markdown)}</pre>`;
|
||||
return (e.description&&e.description.trim())||'<em>No show notes.</em>';
|
||||
}
|
||||
|
||||
/// One enclosure: a play button when the file is here, otherwise what it is and a way to get it.
|
||||
|
||||
@@ -49,6 +49,9 @@ interface Entry {
|
||||
link: string | null;
|
||||
published: number | null;
|
||||
description: string | null;
|
||||
/** The item's Markdown as written, when its feed sends it beside the HTML, and made HTML (#285). */
|
||||
markdown: string | null;
|
||||
markdown_html: string | null;
|
||||
read: boolean;
|
||||
flagged: boolean;
|
||||
image: string | null;
|
||||
|
||||
Reference in New Issue
Block a user