From d0b8cc92b450a1a3204aaacdcbfe0711a1fc4bc7 Mon Sep 17 00:00:00 2001 From: erseco Date: Wed, 3 Jun 2026 13:54:07 +0100 Subject: [PATCH 1/2] feat: shortcode docs, teacher-mode activation, screenshot display Add a dedicated docs/SHORTCODES.md reference and close two capability gaps surfaced while documenting the [exelearning] shortcode: - teacher_mode attribute: embed content with teacher mode already active, by injecting an activation script into the same-origin preview iframe (adds the mode-teacher class, checks the toggler, sets exeTeacherMode). - screenshot attribute (no|poster|only): show the package screenshot.png shipped by eXeLearning >= 4.0.1, either as a click-to-load poster or a standalone image; detected at render time and gracefully falling back to the iframe when absent. Cross-link the new reference from README.md and AGENTS.md. Closes #46 --- AGENTS.md | 1 + README.md | 11 +- docs/SHORTCODES.md | 125 ++++++++++++++++ public/class-shortcodes.php | 266 +++++++++++++++++++++++++++++----- tests/unit/ShortcodesTest.php | 144 ++++++++++++++++++ 5 files changed, 506 insertions(+), 41 deletions(-) create mode 100644 docs/SHORTCODES.md diff --git a/AGENTS.md b/AGENTS.md index 4321924..0f22a54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,7 @@ These are natural-language guidelines for agents to follow when developing the E - Prefer simplicity and clarity: avoid overly complex abstractions. - Load translation strings properly (`__()`, `_e()`), text domain declared in main plugin file. - Keep plugin bootstrap file small (`exelearning.php`), modularize into separate files/classes with specific responsibility. +- Keep the reference docs under `docs/` in sync with the code: `docs/SHORTCODES.md` documents the `[exelearning]` shortcode and all its attributes (including `teacher_mode` and `screenshot`), and `docs/HOOKS.md` documents the developer actions and filters. When you add or change a shortcode attribute or a hook, update the matching doc in the same change. ## Aider-specific usage diff --git a/README.md b/README.md index e160055..0e859ef 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,16 @@ EXELEARNING_EDITOR_REF=my-feature EXELEARNING_EDITOR_REF_TYPE=branch make build- ``` [exelearning id="123"] ``` -Replace `123` with the attachment ID of your ELPX file. +Replace `123` with the attachment ID of your ELPX file. The shortcode also +accepts options to set the height, activate teacher mode, show a download button, +or display the package screenshot: + +``` +[exelearning id="123" height="800" teacher_mode="1" screenshot="poster"] +``` + +See [`docs/SHORTCODES.md`](docs/SHORTCODES.md) for the full shortcode reference, +all attributes, and examples. ### Viewing ELPX Files diff --git a/docs/SHORTCODES.md b/docs/SHORTCODES.md new file mode 100644 index 0000000..05f31d7 --- /dev/null +++ b/docs/SHORTCODES.md @@ -0,0 +1,125 @@ +# Shortcode reference + +The eXeLearning plugin exposes a single shortcode, `[exelearning]`, to embed an +uploaded `.elpx` package anywhere classic-editor content is rendered (posts, +pages, widgets, or any context that runs `do_shortcode()`). + +The shortcode works directly with **WordPress attachments**: you reference an +uploaded package by its Media Library attachment ID. No custom post type is +involved. + +```text +[exelearning id="123"] +``` + +Replace `123` with the attachment ID of your `.elpx` file (visible in the Media +Library, e.g. in the URL `…/upload.php?item=123`). + +## How it renders + +- If the attachment has been extracted and contains a previewable `index.html`, + the shortcode renders a sandboxed, same-origin `', + $iframe_src_attr, + $height, + $is_poster ? ' display: none;' : '', + esc_attr( $title ) + ); + return sprintf( '
@@ -233,18 +370,91 @@ private function render_preview( $title, $preview_url, $height, $file_url, $teac
- - - ', esc_attr( $unique_id ), - esc_html( $title ), - '' !== $download_html ? $download_html : $fallback_download, - esc_attr__( 'View fullscreen', 'exelearning' ), - esc_url( $preview_url ), - $height, - esc_attr( $title ), - esc_attr( $unique_id ), - $teacher_mode_visible ? 'true' : 'false' + $body ); } } diff --git a/tests/unit/ShortcodesTest.php b/tests/unit/ShortcodesTest.php index db04b23..aae0647 100644 --- a/tests/unit/ShortcodesTest.php +++ b/tests/unit/ShortcodesTest.php @@ -299,4 +299,148 @@ public function test_register_shortcodes_exists() { public function test_display_exelearning_exists() { $this->assertTrue( method_exists( $this->shortcodes, 'display_exelearning' ) ); } + + /** + * Create an attachment with a previewable extraction directory. + * + * @param string $hash Extraction hash (40 hex chars). + * @param bool $with_screenshot Whether to also write a screenshot.png fixture. + * @return int Attachment ID. + */ + private function create_previewable_attachment( $hash, $with_screenshot = false ) { + $attachment_id = $this->factory->attachment->create(); + update_post_meta( $attachment_id, '_exelearning_extracted', $hash ); + update_post_meta( $attachment_id, '_exelearning_has_preview', '1' ); + + if ( $with_screenshot ) { + $upload_dir = wp_upload_dir(); + $dir = trailingslashit( $upload_dir['basedir'] ) . 'exelearning/' . $hash; + wp_mkdir_p( $dir ); + file_put_contents( $dir . '/screenshot.png', 'PNG' ); // phpcs:ignore + } + + return $attachment_id; + } + + /** + * Test teacher_mode is off by default (no activation script injected). + */ + public function test_teacher_mode_off_by_default() { + $attachment_id = $this->create_previewable_attachment( str_repeat( '1', 40 ) ); + + $result = $this->shortcodes->display_exelearning( array( 'id' => $attachment_id ) ); + + $this->assertStringContainsString( 'assertStringNotContainsString( 'mode-teacher', $result ); + $this->assertStringNotContainsString( 'exeTeacherMode', $result ); + } + + /** + * Test teacher_mode="1" injects the teacher-mode activation script. + */ + public function test_teacher_mode_activation() { + $attachment_id = $this->create_previewable_attachment( str_repeat( '2', 40 ) ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'teacher_mode' => '1', + ) + ); + + $this->assertStringContainsString( 'assertStringContainsString( 'mode-teacher', $result ); + $this->assertStringContainsString( 'exeTeacherMode', $result ); + } + + /** + * Test screenshot="only" renders just the image and no iframe. + */ + public function test_screenshot_only_renders_image_without_iframe() { + $hash = str_repeat( '3', 40 ); + $attachment_id = $this->create_previewable_attachment( $hash, true ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'screenshot' => 'only', + ) + ); + + $this->assertStringContainsString( 'assertStringContainsString( 'screenshot.png', $result ); + $this->assertStringContainsString( $hash, $result ); + $this->assertStringNotContainsString( 'create_previewable_attachment( $hash, true ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'screenshot' => 'poster', + ) + ); + + $this->assertStringContainsString( 'screenshot.png', $result ); + $this->assertStringContainsString( 'exelearning-poster', $result ); + // The proxy URL must be available so the embed can load on click. + $this->assertStringContainsString( 'exelearning/v1/content/', $result ); + } + + /** + * Test screenshot="only" falls back to the iframe embed when no screenshot exists. + */ + public function test_screenshot_only_falls_back_without_screenshot() { + $attachment_id = $this->create_previewable_attachment( str_repeat( '5', 40 ) ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'screenshot' => 'only', + ) + ); + + $this->assertStringContainsString( 'assertStringNotContainsString( 'screenshot.png', $result ); + } + + /** + * Test screenshot="poster" falls back to the iframe embed when no screenshot exists. + */ + public function test_screenshot_poster_falls_back_without_screenshot() { + $attachment_id = $this->create_previewable_attachment( str_repeat( '6', 40 ) ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'screenshot' => 'poster', + ) + ); + + $this->assertStringContainsString( 'assertStringNotContainsString( 'exelearning-poster', $result ); + } + + /** + * Test screenshot="no" (default) keeps the current iframe behavior. + */ + public function test_screenshot_no_keeps_iframe() { + $attachment_id = $this->create_previewable_attachment( str_repeat( '7', 40 ), true ); + + $result = $this->shortcodes->display_exelearning( + array( + 'id' => $attachment_id, + 'screenshot' => 'no', + ) + ); + + $this->assertStringContainsString( 'assertStringNotContainsString( 'exelearning-poster', $result ); + } } From a5f713f65b5e20f9469b45dc73d64eee4bc519bd Mon Sep 17 00:00:00 2001 From: erseco Date: Wed, 3 Jun 2026 14:00:47 +0100 Subject: [PATCH 2/2] i18n: add Spanish translation for shortcode poster string Register the new "Load interactive content" string in the POT/PO catalogs and translate it to Spanish, then rebuild the es_ES .mo. --- languages/exelearning-es_ES.mo | Bin 18886 -> 18956 bytes languages/exelearning-es_ES.po | 3 +++ languages/exelearning.pot | 3 +++ 3 files changed, 6 insertions(+) diff --git a/languages/exelearning-es_ES.mo b/languages/exelearning-es_ES.mo index c906c8381161d9110f237ab4db8e16fe195366ba..0a99b24cfca4333abdc3b9263617506f270a7131 100644 GIT binary patch delta 3285 zcmYk;3rv<(9LMoL$}1uWh+ITb5HF|*m2wgAf_T3WOH&7w6Gam*FG;9b*vraVITJ6~ zmT6g;MiZ-%Y1+j5R?`g4#b#RCRhzhUGcv=zKb~hR27JzWc%F0q=YRg^c{#P&Z(WU_ z?{c{R7USy_Ke7Bw?Vz4nW$=II(p*h{EFQr}(bL&11Pd_$OEDFf;Y8epk$4lkpog2g zV>E{15X?98SuTwrE^I;%zKKI|C&pnDYTy>^i^)8ljnmK{*P~+IjND>daUj-W7M{n$ z7!qa{gJ)3p{fY_9Z+B=Aq{R>>O-sZG9ELfVhYD;xYQh@S%3jB=_#rZf9Yn@>E zd>0i^B&*PU{csSDME+!@n2xK_r__H!Lmojzej3~1IaFpY;B@>E`L+y}ABa;?f1l}E zf=Y2YM&UZ_f$yRMY(Um#-=VhPW+eI73R=0K0ekaQM@&LRo{D-P&+X5_8T417CO(T~ z*DfJL*iF>Ho@i(9BT(byqwZUXTEH{Nw^c`ze+{&oo-(ixmAa#-2OCjaa~+k!2ric3 zDAa^IQ2`!A=C(7a@qR)*cMG-Gv81IpPDK@~7qwN(eKZvD22=*#LPc7O+WSN9^^2$p ze#Q}a4~faL7)2SIhFr2D)C!lQYH2NM!g^G39z@M|3{~vDMz?Vlb>nUKhPybJejpo| zg6XdFF^_%?YAddyRyd3eejFF#gLn$_@lRAHCQ!MRxD2zf86VR5AK1%@vrr`?YC1`t4Tksj`cs61@{)Oq-y^m8%kD%@^M%9KFwH1}v zk@;;64Hez1*dDiIupj3emD;_$MYQtENXjg@pVJ?Sne>;SwrCfUZM%SN#fK{D*#6E! z2B6M=6|!qqgFdbFBN{4(?hRKUMsA^wfZ+)OI9 z11>;KxD?}YKMuzx)B+-javWx%2dk3FzXEuP3mUi@Rh{o(0bauUm_Zt}@(lJv6Hdnr zoQJBJH&B7rp)zs|bMYc-qFz+C0_=+%EK5SYKgRoLgwc2mJK<8)15YD!+jBS?_aKY4 zW>kRPDFl*cGmynumD{gF1=i&D!-nutOFx@m)TMb*HL?*^Bfh;fG;kAgB+Z|*r_?84 z9A@G$EI>uR2`6J624Rba=XZK&->gI{*8fhMjb8IFEYp zcckvj!ofYH?R`&smx@eGB67@K{*Cvwd*#FreBN7+)-q=?22nRX{?|>1~uOS>_z?A zIrqXJr~ra8oqs%Ha2)+YR4wd473){16@80gcn7ue0187Z>w>&GtUoHi$*2s@!a`h) z3a|-%N?i*L-Jt)gw9@{_;%zLdYM({j_dF`~YhAZtB>lba-%nsT{nMzL`W{m;h%=~S z&A|d(j3sz-IQvii*q{++b8t4Q-++qrHtNBLNAl9ZRj6Zi9~s+nM>$pe9CFg^ePo-h z35zf!$Ep5BuG?`V*S|(D&K>PzP#TS#v6VQ5(KX?5)QX$26W&1&`i*hkhykcnr(--$ z!LGOjm60k`0Iy*v-a^)5?Z!G=l7n&d^L#WE`BGH1Z%3`{1nNN*fR0%hD)q6b8c0Fy zc?L$~Xl#!KxY&=6V$`eq3o2P#(Sll7-w95}@{uH(ZxxM18V9f^{(yR+w4x?Xn&^CZ zWS~}7jSBQVw|^RY(7%e^uoa^)dXkg+6x8^|n1HWfAnrl#54TTgsJhQ$2>w%F8r%>V s5nt-9m|tEzucCCt{P=lgbi5VyeWEJjqRZWGX-V1tZYrzan3fjtFEZwDHUIzs delta 3224 zcmYM#f2`J39S88kk1K*)-~yLRA%X~8erq6t$OVFX4g3*0)H@(6hyjI`k;@p$V}S<5 zSQmdR%+}gm-6;(QwnQmKyUbse#G*emVXKs7H5t*Cmh^h_JX`F!&+DA;bH3m6IiK@6 zUv@m)?Z`vjDsK<$`AG4v%iqQRS}#bilso$V-}#z%>bG(qZ{bYvbv`pqT^Di~GjKGsf*HJ(H!&0b zn6g=(rd!Jq=7Dc9d;T7C-`J+=xVcQ=H`AxA_Lb*sYLfq~e1{83+4qb9 zKQqB0w(uCmQW`uZnHo=*%2Z~BH!wA{f|+nTQj_| z!g?DRXY)p`W47WTv%>y1_IA$Y75o7g^EXT;E>W@fav^82i=SY*-j?MdSB_7tlz?|T*uGypS+7pMmiDv6|?f0@|_}H!DMz#t^ZZ}lzV)g&jg3&{C8?3 z^@o`~J;rvv!wj6GLZ|aadg+zT%oco?IhLIq$v1cvPcyYN?y~CmnM`f8FyDU>^K?nzQt@OxsAxbHg&gDvt>tH!`MQ|r9G@WnS8AMaAw@NKVzt`8#02zBj^m?D#LsgXf5)VFvP!*x zEzE?i9Lb%W&ArS5{>C;oh_;UHOaQk}mj66>wF{}v4P45nd5-<0AuI1^KQiG2&fqMj zX4W%-ZecRAoAbG!ndmGN;D>aeN_Ul>_s4K16VoaheKcB`8y3^tF zC96zQ5EfP1nf?|gu)VeZyPT>&(3g5FZA^`$rs%@wYYKWCDMew!ZwGc_F>e73^bwb6W0TZ*E~dcUG;G$7=(gWNv(s>aM)S zG3?>&oi9E%=(jO7(av7HpLy_B4(2X;b(EiQC6Cmuk9Rhci8;(XD_EcNzft2_7q-;~ zyuoDT472hEc}?mju`d_Zyp2uzUtlt}k#?tizvk<_TmQezgzKdGNwwmGBDN>T&Sp~HTJttG>aVT+zKH|%A7qMYH|<*KVv6zuF69_!aTP067Uj?U zJcqW(e=cljsYd!!=Ee)>s8CMhd>*7{mH#l+J9BRJ4Y`uGx9s9_zD)I7M$fC-&JO+U z{4y_gq&Dz7yoYu3E7b|_GfPs_$v)i2IzGp|2@f-=exD;*x1jn;9m`~778Aft?9V;4 zmU4*Mk`Fk7y;`e*H#0@vUeU