Skip to content

Commit adc2f0c

Browse files
committed
Create GitHubMarkdownPreview.js
1 parent d7b9855 commit adc2f0c

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**** GitHubMarkdownPreview - renders "Value" via the GitHub Markdown API ****/
2+
3+
// the Markdown text held in the widget's "Value" property is POSTed to GitHub's
4+
// "/markdown" endpoint and the returned (already sanitised) HTML is displayed.
5+
// "Token" may hold an optional GitHub access token - when present it is sent as
6+
// a "Bearer" authorisation, lifting the rate limit and enabling private context.
7+
// Any failure (network, HTTP status, abort) is shown as a readable error message.
8+
//
9+
// Configuration fields (all optional):
10+
// "Endpoint" - API URL (default 'https://api.github.com/markdown')
11+
// "APIVersion" - value of the "X-GitHub-Api-Version" header (default '2022-11-28')
12+
// "Mode" - 'markdown' (default) or 'gfm' for GitHub-Flavoured Markdown
13+
// "Context" - 'owner/repo' used to resolve issue/PR references in "gfm" mode
14+
// "Timeout" - request timeout in milliseconds (default 15000)
15+
// "Color" - text colour of the rendered output
16+
17+
const DefaultEndpoint = 'https://api.github.com/markdown'
18+
const DefaultVersion = '2022-11-28'
19+
const DefaultTimeout = 15000
20+
21+
/**** messageFromErrorBody - pulls a readable detail out of GitHub's response ****/
22+
23+
function messageFromErrorBody (Body) {
24+
const Text = String(Body ?? '').trim()
25+
if (Text === '') { return '' }
26+
try {
27+
const Parsed = JSON.parse(Text)
28+
return String(Parsed?.message ?? Text)
29+
} catch (Signal) {
30+
return Text // body was not JSON - show as-is
31+
}
32+
}
33+
34+
/**** actual behavior script ****/
35+
36+
export default async function ({ on, me, my, html, Configuration }) {
37+
const Endpoint = (Configuration?.Endpoint ?? DefaultEndpoint)
38+
const APIVersion = (Configuration?.APIVersion ?? DefaultVersion)
39+
const Mode = (Configuration?.Mode ?? undefined)
40+
const Context = (Configuration?.Context ?? undefined)
41+
const Timeout = (
42+
(typeof Configuration?.Timeout === 'number') && (Configuration.Timeout > 0)
43+
? Configuration.Timeout : DefaultTimeout
44+
)
45+
46+
/**** "State" holds the private request status (no persistence, no re-render) ****/
47+
48+
function State () {
49+
return (my.own.GitHubMarkdown ??= { Key:undefined, Status:'idle', HTML:'', Message:'' })
50+
}
51+
52+
/**** RequestKeyFor - identifies a request so unchanged inputs are not refetched ****/
53+
54+
function RequestKeyFor (Text, Token) {
55+
return JSON.stringify([ Text, Token, Mode, Context, Endpoint, APIVersion ])
56+
}
57+
58+
/**** fetchRendering - POSTs the Markdown and stores the resulting HTML or error ****/
59+
60+
async function fetchRendering (Text, Token, Key) {
61+
const Controller = new AbortController()
62+
const Timer = setTimeout(() => Controller.abort(), Timeout)
63+
64+
const Headers = {
65+
'Content-Type':'application/json',
66+
'X-GitHub-Api-Version':APIVersion,
67+
}
68+
if (Token !== '') { Headers['Authorization'] = 'Bearer '+Token }
69+
70+
const Payload = { text:Text }
71+
if (Mode != null) { Payload.mode = Mode }
72+
if (Context != null) { Payload.context = Context }
73+
74+
try {
75+
const Response = await fetch(Endpoint, {
76+
method:'POST', headers:Headers,
77+
body:JSON.stringify(Payload), signal:Controller.signal,
78+
})
79+
const Body = await Response.text()
80+
81+
const currentState = State()
82+
if (currentState.Key !== Key) { return } // newer request superseded this one
83+
84+
if (! Response.ok) {
85+
const Detail = messageFromErrorBody(Body)
86+
currentState.Status = 'error'
87+
currentState.Message = (
88+
'GitHub Markdown API: '+Response.status+' '+Response.statusText+
89+
(Detail === '' ? '' : ' - '+Detail)
90+
)
91+
} else {
92+
currentState.Status = 'done'
93+
currentState.HTML = Body
94+
}
95+
} catch (Signal) {
96+
const currentState = State()
97+
if (currentState.Key !== Key) { return }
98+
currentState.Status = 'error'
99+
currentState.Message = (
100+
Signal?.name === 'AbortError'
101+
? 'GitHub Markdown API: request timed out after '+Timeout+' ms'
102+
: 'GitHub Markdown API: '+(Signal?.message ?? String(Signal))
103+
)
104+
} finally {
105+
clearTimeout(Timer)
106+
}
107+
me.rerender() // reflect the new status on screen
108+
}
109+
110+
/**** on 'update' - start a fresh request whenever the inputs actually change ****/
111+
112+
on('update', () => {
113+
const Text = String(my.Value ?? '')
114+
const Token = String(my.Token ?? '').trim()
115+
const Data = State()
116+
117+
if (Text.trim() === '') { // nothing to render
118+
Data.Key = undefined; Data.Status = 'idle'
119+
Data.HTML = ''; Data.Message = ''
120+
return
121+
}
122+
123+
const Key = RequestKeyFor(Text, Token)
124+
if (Data.Key === Key) { return } // identical request already handled
125+
126+
Data.Key = Key; Data.Status = 'loading'; Data.Message = ''
127+
fetchRendering(Text, Token, Key) // fire and forget - re-renders later
128+
})
129+
130+
/**** Placeholder - centred hint shown while idle or loading ****/
131+
132+
function Placeholder (Message, Colour) {
133+
return html`
134+
<div style=${{
135+
display:'flex', alignItems:'center', justifyContent:'center',
136+
width:'100%', height:'100%',
137+
color:(Colour ?? '#999999'), fontSize:'13px', textAlign:'center', padding:'4px',
138+
}}>${Message}</div>
139+
`
140+
}
141+
142+
/**** renderer callback ****/
143+
144+
on('render', () => {
145+
const Data = State()
146+
147+
switch (true) {
148+
case (Data.Status === 'idle'):
149+
return Placeholder('(Markdown text in "Value")')
150+
case (Data.Status === 'loading'):
151+
return Placeholder('rendering Markdown…')
152+
case (Data.Status === 'error'):
153+
return html`
154+
<div style=${{
155+
width:'100%', height:'100%', overflow:'auto',
156+
padding:'8px', textAlign:'left',
157+
color:'#a00', font:'13px/1.4 ui-monospace,Menlo,Consolas,monospace',
158+
whiteSpace:'pre-wrap',
159+
}}>${Data.Message}</div>
160+
`
161+
default: // 'done' - show the rendered HTML
162+
return html`
163+
<div
164+
class="bc-markdown markdown-body"
165+
style=${{
166+
width:'100%', height:'100%', overflow:'auto',
167+
padding:'0px', textAlign:'left',
168+
color:(my.Color ?? Configuration?.Color),
169+
}}
170+
dangerouslySetInnerHTML=${{ __html:(Data.HTML ?? '') }}
171+
></div>
172+
`
173+
}
174+
})
175+
}

0 commit comments

Comments
 (0)