Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Commit f843e02

Browse files
authored
add support for embeddable iframes (#268)
* add support for embedded iframes * add css support for embedded iframes * add formatting instructions for embedded iframes * pass images through with non-whitelisted alt values * fix tests * review comments fixed * add control print if non-whitelisted domain encountered * add newline to error message
1 parent 1a91cf1 commit f843e02

9 files changed

Lines changed: 1055 additions & 916 deletions

File tree

FORMAT-GUIDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ You can also use this to target specific events, for instance: \
6363
- Put a Youtube video link in the **Description** field of the Alt Text. in the format `https://www.youtube.com/watch?v=[video_ID]`
6464
> Specifying a start time is not supported at this time.
6565
66+
1. Embedded Iframes
67+
68+
Iframes can be embedded by doing:
69+
- Add an image in the document. The image can be a screenshot of the iframe for instance but it doesn't really matter since it won't be displayed but replaced by the embedded iframe.
70+
- Add an "Alt Text" to the image by doing **Cmd+Opt+Y** or **Right click > "Alt Text..."**
71+
- Put a full URL in the **Description** field of the Alt Text. in the format `https://www.domain.com/watch?foo=bar`. Note that for security reasons, iframe embbedding is limited to https URLs.
72+
6673
1. Info Boxes
6774

6875
For additional information that you would like to specially call-out in your codelab, there are two styles of info boxes:

claat/parser/gdoc/parse.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"net/url"
22+
"os"
2223
"sort"
2324
"strconv"
2425
"strings"
@@ -650,9 +651,31 @@ func list(ds *docState) types.Node {
650651
// image creates a new ImageNode out of hn, parsing its src attribute.
651652
// It returns nil if src is empty.
652653
// It may also return a YouTubeNode if alt property contains specific substring.
654+
// or an IframeNode if the alt property contains a URL other than youtube.
653655
func image(ds *docState) types.Node {
654-
if strings.Contains(nodeAttr(ds.cur, "alt"), "youtube.com/watch") {
656+
alt := nodeAttr(ds.cur, "alt")
657+
errorAlt := ""
658+
if strings.Contains(alt, "youtube.com/watch") {
655659
return youtube(ds)
660+
} else if strings.Contains(alt, "https://") {
661+
u, err := url.Parse(nodeAttr(ds.cur, "alt"))
662+
if err != nil {
663+
return nil
664+
}
665+
// For iframe, make sure URL ends in whitelisted domain.
666+
ok := false
667+
for _, domain := range types.IframeWhitelist {
668+
if strings.HasSuffix(u.Hostname(), domain) {
669+
ok = true
670+
break
671+
}
672+
}
673+
if ok {
674+
return iframe(ds)
675+
} else {
676+
errorAlt = "The domain of the requested iframe (" + u.Hostname() + ") has not been whitelisted."
677+
fmt.Fprint(os.Stderr, errorAlt+"\n")
678+
}
656679
}
657680
s := nodeAttr(ds.cur, "src")
658681
if s == "" {
@@ -661,7 +684,11 @@ func image(ds *docState) types.Node {
661684
n := types.NewImageNode(s)
662685
n.Width = styleFloatValue(ds.cur, "width")
663686
n.MutateBlock(findBlockParent(ds.cur))
664-
n.Alt = nodeAttr(ds.cur, "alt")
687+
if errorAlt != "" {
688+
n.Alt = errorAlt
689+
} else {
690+
n.Alt = nodeAttr(ds.cur, "alt")
691+
}
665692
n.Title = nodeAttr(ds.cur, "title")
666693
return n
667694
}
@@ -680,6 +707,20 @@ func youtube(ds *docState) types.Node {
680707
return n
681708
}
682709

710+
func iframe(ds *docState) types.Node {
711+
u, err := url.Parse(nodeAttr(ds.cur, "alt"))
712+
if err != nil {
713+
return nil
714+
}
715+
// Allow only https.
716+
if u.Scheme != "https" {
717+
return nil
718+
}
719+
n := types.NewIframeNode(u.String())
720+
n.MutateBlock(true)
721+
return n
722+
}
723+
683724
// button returns either a text node, if no <a> child element is present,
684725
// or link node, containing the button.
685726
// It returns nil if no content nodes are present.

claat/parser/gdoc/parse_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ func TestMetaTable(t *testing.T) {
180180
}
181181
meta := types.Meta{
182182
Summary: "Test summary",
183-
Authors: "John Smith <user@example.com>",
183+
Authors: "John Smith <user@example.com>",
184184
Categories: []string{"Foo", "Bar"},
185185
Theme: "foo",
186186
Status: clab.Meta.Status, // verified separately
@@ -229,6 +229,8 @@ func TestParseDoc(t *testing.T) {
229229
<p><img src="https://host/small.png" style="height: 10px; width: 25.5px"> icon.</p>
230230
231231
<p><img alt="https://www.youtube.com/watch?v=vid" src="https://yt.com/vid.jpg"></p>
232+
<p><img alt="https://repl.it/?foo=bar" src="https://host/image.png"></p>
233+
<p><img alt="https://example.com/?foo=bar" src="https://host/image.png"></p>
232234
233235
<h3><a name="a3"></a><span>What you&rsquo;ll learn</span></h3>
234236
<ul class="start">
@@ -342,6 +344,16 @@ func TestParseDoc(t *testing.T) {
342344
yt.MutateBlock(true)
343345
content.Append(yt)
344346

347+
iframe := types.NewIframeNode("https://repl.it/?foo=bar")
348+
iframe.MutateBlock(true)
349+
content.Append(iframe)
350+
351+
img = types.NewImageNode("https://host/image.png")
352+
img.Alt = "The domain of the requested iframe (example.com) has not been whitelisted."
353+
para = types.NewListNode(img)
354+
para.MutateBlock(true)
355+
content.Append(para)
356+
345357
h := types.NewHeaderNode(3, types.NewTextNode("What you'll learn"))
346358
h.MutateType(types.NodeHeaderCheck)
347359
content.Append(h)

claat/parser/md/parse.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,9 +638,26 @@ func list(ds *docState) types.Node {
638638
// It returns nil if src is empty.
639639
// It may also return a YouTubeNode if alt property contains specific substring.
640640
func image(ds *docState) types.Node {
641-
if strings.Contains(nodeAttr(ds.cur, "alt"), "youtube.com/watch") {
642-
return youtube(ds)
643-
}
641+
alt := nodeAttr(ds.cur, "alt")
642+
if strings.Contains(alt, "youtube.com/watch") {
643+
return youtube(ds)
644+
} else if strings.Contains(alt, "https://") {
645+
u, err := url.Parse(nodeAttr(ds.cur, "alt"))
646+
if err != nil {
647+
return nil
648+
}
649+
// For iframe, make sure URL ends in whitelisted domain.
650+
ok := false
651+
for _, domain := range types.IframeWhitelist {
652+
if strings.HasSuffix(u.Hostname(), domain) {
653+
ok = true
654+
break
655+
}
656+
}
657+
if ok {
658+
return iframe(ds)
659+
}
660+
}
644661
s := nodeAttr(ds.cur, "src")
645662
if s == "" {
646663
return nil
@@ -682,6 +699,20 @@ func youtube(ds *docState) types.Node {
682699
return n
683700
}
684701

702+
func iframe(ds *docState) types.Node {
703+
u, err := url.Parse(nodeAttr(ds.cur, "alt"))
704+
if err != nil {
705+
return nil
706+
}
707+
// Allow only https.
708+
if u.Scheme != "https" {
709+
return nil
710+
}
711+
n := types.NewIframeNode(u.String())
712+
n.MutateBlock(true)
713+
return n
714+
}
715+
685716
// button returns either a text node, if no <a> child element is present,
686717
// or link node, containing the button.
687718
// It returns nil if no content nodes are present.

claat/render/gen-tmpldata.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ var files = map[string]struct {
3232
file string
3333
html bool
3434
}{
35-
"html": {"template.html", true},
36-
"devsite": {"template-devsite.html", true},
37-
"md": {"template.md", false},
38-
"offline": {"template-offline.html", true},
35+
"html": {"template.html", true},
36+
"devsite": {"template-devsite.html", true},
37+
"md": {"template.md", false},
38+
"offline": {"template-offline.html", true},
3939
}
4040

4141
func main() {

claat/render/html.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ func (hw *htmlWriter) write(nodes ...types.Node) error {
113113
case *types.YouTubeNode:
114114
hw.youtube(n)
115115
hw.writeBytes(newLine)
116+
case *types.IframeNode:
117+
hw.iframe(n)
118+
hw.writeBytes(newLine)
116119
}
117120
if hw.err != nil {
118121
return hw.err
@@ -371,3 +374,8 @@ func (hw *htmlWriter) youtube(n *types.YouTubeNode) {
371374
`autoplay; encrypted-media; gyroscope; picture-in-picture" `+
372375
`allowfullscreen></iframe>`, n.VideoID)
373376
}
377+
378+
func (hw *htmlWriter) iframe(n *types.IframeNode) {
379+
hw.writeFmt(`<iframe class="youtube-video" src="%s"></iframe>`,
380+
n.URL)
381+
}

0 commit comments

Comments
 (0)