Add HTML5 article rendering with embedded content viewers#14548
Add HTML5 article rendering with embedded content viewers#14548rtibbles wants to merge 5 commits into
Conversation
Build Artifacts
Smoke test screenshot |
4735d6d to
f00e02e
Compare
npm Package VersionsWarning The following packages have changed files but no version bump:
If these changes affect published code, consider bumping the version. |
d597e47 to
5da5ce9
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Some things I need to update before undrafting.
| import useContentViewer, { contentViewerProps } from 'kolibri/composables/useContentViewer'; | ||
| import useContentViewer from 'kolibri/composables/useContentViewer'; | ||
|
|
||
| In order to log data about users viewing content, the component should emit ``startTracking``, ``updateProgress``, and ``stopTracking`` events, using the Vue ``$emit`` method. ``startTracking`` and ``stopTracking`` are emitted without any arguments, whereas ``updateProgress`` should be emitted with a single value between 0 and 1 representing the current proportion of progress on the content. |
There was a problem hiding this comment.
Question for reviewers - should I move the entire public API surface into the composable? Just make methods for startTracking, stopTracking and updateProgress and skip the emit altogether?
| allow_object_tag = False | ||
|
|
||
| @classmethod | ||
| def all_css_selectors(cls): |
There was a problem hiding this comment.
Using css_selectors allows for specific content viewers to only render certain DOM nodes if they have particular attributes present, for example.
| from le_utils.constants.file_formats import BLOOMPUB | ||
| from le_utils.constants.file_formats import H5P | ||
| from le_utils.constants.file_formats import HTML5 | ||
| from le_utils.constants.file_formats import HTML5_ARTICLE |
There was a problem hiding this comment.
Need to add this to allow larger media files to be loaded via the zip content backend from kpub files.
| :style="{ width: iframeWidth }" | ||
| @changeFullscreen="isInFullscreen = $event" | ||
| > | ||
| <div |
There was a problem hiding this comment.
We had copied this across HTML5, EPUB, PDF, and Bloompub - rule of three more than applied!
| class DocumentEPUBRenderAsset(content_hooks.ContentRendererHook): | ||
| bundle_id = "main" | ||
| presets = (format_presets.EPUB,) | ||
| allow_object_tag = True |
There was a problem hiding this comment.
We use this as a shorthand to let the <object> tag be used for these kinds of files (but we use the preset logic to work out which files in the object tag they should render and generate CSS selectors accordingly).
5da5ce9 to
fdb1287
Compare
fdb1287 to
e307c3c
Compare
e307c3c to
4eb097a
Compare
4eb097a to
e714f78
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Some open questions, some things I can revisit now before review.
| this.visitedPages = this.savedVisitedPages || {}; | ||
| }, | ||
| beforeMount() { | ||
| global.ePub = Epub; |
There was a problem hiding this comment.
This wasn't needed - I'm not sure why it was ever here.
| window.removeEventListener('mousedown', this.handleMouseDown, { passive: true }); | ||
| clearInterval(this.updateContentStateInterval); | ||
| }, | ||
| destroyed() { |
There was a problem hiding this comment.
Cleanup was unnecessary too.
| }; | ||
| }, | ||
| props: { | ||
| ...contentViewerProps, |
There was a problem hiding this comment.
contentViewerProps is no longer a thing, so we just explicitly define this here.
| }; | ||
| }, | ||
| props: { | ||
| ...contentViewerProps, |
There was a problem hiding this comment.
This component was duplicated for the courses work - so do the same updates here too.
| @@ -0,0 +1,185 @@ | |||
| import { ref, computed } from 'vue'; | |||
There was a problem hiding this comment.
Taking the opportunity of the composable refactor to add test coverage here.
| setup(props, context) { | ||
| const { windowBreakpoint } = useKResponsiveWindow(); | ||
| const { defaultFile, contentDirection } = useContentViewer(props, context); | ||
| const { |
There was a problem hiding this comment.
Note that this will probably need rebasing if the Perseus upgrade PR lands first.
| @@ -0,0 +1,213 @@ | |||
| import { render, screen, waitFor } from '@testing-library/vue'; | |||
There was a problem hiding this comment.
There's a decision encoded in this spec and the code to have 50% of progress be from scrolling through the article, 50% from any nested viewers. I don't know what the right call is here.
| } | ||
| return acc; | ||
| }, {}); | ||
| const ALLOWED_URI_REGEXP = buildAllowedUriRegexp(allowedOrigins); |
There was a problem hiding this comment.
This is required to allow the "large file" fallback used in kolibri-zip that directly defers to the zipcontent endpoint.
| ); | ||
| } | ||
|
|
||
| const safeAttrs = {}; |
There was a problem hiding this comment.
Previously arbitrary props could be passed through to viewer plugin components, creating implicit APIs that bypassed the content viewer. This tightens this up.
…nents to use the viewer props, and instead direct all data via the composable api.
…ction Replace the Vuex store module with composables for instance-specific state management. Extract VideoPlayer and implement AudioPlayer with custom transport controls, sticky player, and inline transcript. Add useMediaProgress composable for shared progress tracking logic.
ContentViewer now generates unique viewer IDs and appends them to all interaction events, allowing parent components to track which embedded viewer emitted each event. SafeHtml5RendererIndex uses this to aggregate progress from multiple embedded viewers (e.g., videos in HTML5 content), combining scroll-based progress with embedded viewer progress using dynamic weighting.
Replace v-bind="$attrs" with explicit class="safe-html" and remove inheritAttrs: false, reducing the component API surface area. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e714f78 to
7b138aa
Compare
Summary
Refactors the content viewer architecture to support rendering HTML content directly in the DOM (rather than exclusively in sandboxed iframes), and migrates the MediaPlayer from Vuex to composables.
Key changes:
useMediaPlayercomposableViewerToolbarcomponent replaces per-viewer toolbar implementations (HTML5, PDF, EPUB)References
Reviewer guidance
To test: load any HTML5 content, PDF, EPUB, video, or audio content and verify it renders and plays correctly. For HTML5 content with embedded media, use the Kolibri QA Channel under HTML5 > HTML5 Article.
HTML5 Articles with embedded content use a blended progress model: 50% from scroll progress, 50% from embedded content completion. The 50/50 split is arbitrary and may need tuning — worth scrutinizing whether this feels right in practice.
Implementation involved judgment calls that may deviate from the design specs — worth comparing against the original designs during review.
Risky areas:
packages/kolibri/internal/pluginMediator.js— viewer registration now supports DOM element viewers alongside preset-based viewers; this is the core extension pointpackages/kolibri/components/internal/ContentViewer/index.js— major rewrite from a simple wrapper to a render-function component withprovide/injectcontext, viewer ID tracking, and DOM element file extractionkolibri/core/content/hooks.py— newcss_selectorsandallow_object_tagonContentRendererHook; theall_css_selectorsclassmethod uses module-level cachingStandalone players (desktop)
Embedded in HTML5 article (desktop, 1280px)
Embedded video player
Screencast.From.2026-04-07.19-26-04.mp4
Sticky audio player
Screencast.From.2026-04-07.19-24-22.mp4
Embedded in HTML5 article (mobile, 412px)
AI usage
Implementation was done collaboratively with Claude Code (Opus). I directed the architecture and made design decisions; Claude helped with implementation, test writing, and iterating on the embedded content rendering approach. All code was reviewed and verified against a running dev server.