├── .distignore
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── assets
├── admin.css
├── common.js
├── location.css
├── location.js
├── mm.png
├── remove-extra-margin.css
├── webmention-icons.svg
├── webmention-legacy.js
└── webmention.js
├── blocks
├── bookmark
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ └── block.json
├── context
│ ├── block.asset.php
│ ├── block.js
│ ├── block.json
│ └── editor.css
├── facepile-content
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ ├── block.json
│ └── render.php
├── facepile
│ ├── block.asset.php
│ ├── block.js
│ ├── block.json
│ └── render.php
├── like
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ └── block.json
├── link-preview
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ ├── block.json
│ └── render.php
├── location
│ ├── block.asset.php
│ ├── block.js
│ ├── block.json
│ └── render.php
├── reply
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ └── block.json
├── repost
│ ├── block.asset.php
│ ├── block.css
│ ├── block.js
│ └── block.json
└── syndication
│ ├── block.asset.php
│ ├── block.js
│ ├── block.json
│ └── render.php
├── build
└── vendor
│ ├── autoload.php
│ ├── composer
│ ├── ClassLoader.php
│ ├── InstalledVersions.php
│ ├── LICENSE
│ ├── autoload_classmap.php
│ ├── autoload_files.php
│ ├── autoload_namespaces.php
│ ├── autoload_psr4.php
│ ├── autoload_real.php
│ ├── autoload_static.php
│ ├── installed.php
│ └── platform_check.php
│ ├── masterminds
│ └── html5
│ │ ├── CREDITS
│ │ └── src
│ │ ├── HTML5.php
│ │ └── HTML5
│ │ ├── Elements.php
│ │ ├── Entities.php
│ │ ├── Exception.php
│ │ ├── InstructionProcessor.php
│ │ ├── Parser
│ │ ├── CharacterReference.php
│ │ ├── DOMTreeBuilder.php
│ │ ├── EventHandler.php
│ │ ├── FileInputStream.php
│ │ ├── InputStream.php
│ │ ├── ParseError.php
│ │ ├── Scanner.php
│ │ ├── StringInputStream.php
│ │ ├── Tokenizer.php
│ │ ├── TreeBuildingRules.php
│ │ └── UTF8Utils.php
│ │ └── Serializer
│ │ ├── HTML5Entities.php
│ │ ├── OutputRules.php
│ │ ├── RulesInterface.php
│ │ └── Traverser.php
│ ├── mf2
│ └── mf2
│ │ └── Mf2
│ │ └── Parser.php
│ ├── michelf
│ └── php-markdown
│ │ ├── Michelf
│ │ ├── Markdown.inc.php
│ │ ├── Markdown.php
│ │ ├── MarkdownExtra.inc.php
│ │ ├── MarkdownExtra.php
│ │ ├── MarkdownInterface.inc.php
│ │ └── MarkdownInterface.php
│ │ └── Readme.php
│ ├── ralouphie
│ └── mimey
│ │ ├── license
│ │ ├── mime.types
│ │ ├── mime.types.php
│ │ └── src
│ │ ├── MimeMappingGenerator.php
│ │ ├── MimeTypes.php
│ │ └── MimeTypesInterface.php
│ └── scoper-autoload.php
├── composer.json
├── composer.lock
├── includes
├── class-blocks.php
├── class-feeds.php
├── class-location.php
├── class-micropub-compat.php
├── class-options-handler.php
├── class-parser.php
├── class-plugin.php
├── class-post-types.php
├── class-preview-cards.php
├── class-theme-mf2.php
├── commands
│ └── class-commands.php
├── functions.php
└── webmention
│ ├── class-webmention-parser.php
│ ├── class-webmention-receiver.php
│ ├── class-webmention-sender.php
│ └── class-webmention.php
├── indieblocks.code-workspace
├── indieblocks.php
├── languages
└── indieblocks.pot
├── phpcs.xml
├── readme.txt
├── scoper.inc.php
├── scoper.sh
└── templates
├── feed-atom.php
└── feed-rss2.php
/.distignore:
--------------------------------------------------------------------------------
1 | /.wordpress-org
2 | /.git
3 | /.github
4 | /node_modules
5 | /tests
6 | /vendor
7 | /svn
8 |
9 | .distignore
10 | .gitignore
11 | .phpunit.result.cache
12 | .prettierrc
13 | README.md
14 | bootstrap.php
15 | composer.json
16 | composer.lock
17 | phpcs.xml
18 | phpunit.xml
19 | *.code-workspace
20 | scoper.*
21 | *.bak.*
22 | *.cache
23 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to WordPress.org
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | jobs:
7 | tag:
8 | name: New tag
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@master
12 | - name: WordPress Plugin Deploy
13 | uses: 10up/action-wordpress-plugin-deploy@stable
14 | env:
15 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
16 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ignore all files that start with ~
2 | ~*
3 |
4 | # ignore OS-generated files
5 | ehthumbs.db
6 | Thumbs.db
7 |
8 | # ignore Editor files
9 | *.sublime-project
10 | *.sublime-workspace
11 | *.komodoproject
12 |
13 | # ignore log files, databases and shell scripts
14 | *.log
15 | *.sql
16 | *.sqlite
17 | #*.sh
18 |
19 | # ignore compiled files
20 | *.com
21 | *.class
22 | *.dll
23 | *.exe
24 | *.o
25 | *.so
26 |
27 | # ignore packaged files
28 | *.7z
29 | *.dmg
30 | *.gz
31 | *.iso
32 | *.jar
33 | *.rar
34 | *.tar
35 | *.zip
36 |
37 | /svn/*
38 | /vendor/*
39 | *.bak.*
40 | *.cache
41 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "tabWidth": 4,
4 | "printWidth": 120,
5 | "singleQuote": true,
6 | "trailingComma": "es5",
7 | "bracketSameLine": false,
8 | "bracketSpacing": true,
9 | "parenSpacing": true,
10 | "semi": true,
11 | "arrowParens": "always"
12 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IndieBlocks
2 | Use blocks, and, optionally, "short-form" post types to more easily "IndieWebify" your WordPress site.
3 |
4 | ## Why
5 | This plugin lets you:
6 |
7 | * Add a "microblog" to either a new or existing WordPress site
8 | * Add Webmention support to enable rich _cross-site_ conversations
9 | * Add "microformats" to your WordPress site's front end and posts, so that Webmention endpoints are able to correctly interpret your posts
10 |
11 | ## How
12 | IndieBlocks provides a number of blocks that can be used to add microformats to posts _without having to tweak or know HTML_. It also adds a number of "short-form" custom post types to more easily add a "microblog" to your site. (Both are optional, like, well, all of IndieBlocks features.)
13 |
14 | IndieBlocks also "hooks" into the [Micropub](https://wordpress.org/plugins/micropub/) plugin, so that Micropub posts are given the correct post type, and, when applicable, "microformats" blocks.
15 |
16 | If you're running a "block theme," it will attempt to add "microformats" to it. (If your theme already supports microformats, just leave this part disabled.)
17 |
18 | There's also Webmention, which can be enabled for posts, notes, and likes (and any other post types of your choosing). This, in combination with the aforementioned microformats and this plugin's blocks, enables richer _cross-site_ conversations. (If you're perfectly happy with your existing Webmention setup, here, too, you can simply leave this setting disabled.)
19 |
20 | ## Classic Editor and Themes
21 | IndieBlocks, despite its name, runs just fine on sites that have the Classic Editor plugin installed, or don't (yet) use a "block theme." The Custom Post Types and Webmention options will just work, as will most of its smaller "modules." Microformats, however, will need to be added some other way. (It's absolutely okay to run a "classic" theme with microformats support and add microformats to posts by hand or through a different plugin.)
22 |
23 | ## Modules
24 | * **Blocks:** Add microformats to posts without having to think about HTML. There's also a couple blocks for use in theme templates (like the "Facepile" block for webmentions, or the "Link Preview" block).
25 | * **Block theme "enhancements":** Automatic microformats support for any (!) "block theme."
26 | * **Webmention:** Send and receive webmentions, for _actual cross-site conversations_.
27 | * **Custom post types:** Add "note" (i.e., a "microblog") and "like" sections.
28 | * **Location**: Add basic location and weather info to posts.
29 | * **Link preview cards**: Grab basic metadata from linked pages (to be used with the "Link Preview" block).
30 |
31 |
32 |
--------------------------------------------------------------------------------
/assets/admin.css:
--------------------------------------------------------------------------------
1 | .repost .dashboard-comment-wrap,
2 | .like .dashboard-comment-wrap,
3 | .favorite .dashboard-comment-wrap,
4 | .tag .dashboard-comment-wrap,
5 | .bookmark .dashboard-comment-wrap,
6 | .listen .dashboard-comment-wrap,
7 | .watch .dashboard-comment-wrap,
8 | .read .dashboard-comment-wrap,
9 | .follow .dashboard-comment-wrap,
10 | .mention .dashboard-comment-wrap,
11 | .reacji .dashboard-comment-wrap {
12 | padding-inline-start: 63px;
13 | }
14 |
15 | .repost .dashboard-comment-wrap .comment-author,
16 | .like .dashboard-comment-wrap .comment-author,
17 | .favorite .dashboard-comment-wrap .comment-author,
18 | .tag .dashboard-comment-wrap .comment-author,
19 | .bookmark .dashboard-comment-wrap .comment-author,
20 | .listen .dashboard-comment-wrap .comment-author,
21 | .watch .dashboard-comment-wrap .comment-author,
22 | .read .dashboard-comment-wrap .comment-author,
23 | .follow .dashboard-comment-wrap .comment-author,
24 | .mention .dashboard-comment-wrap .comment-author,
25 | .reacji .dashboard-comment-wrap .comment-author {
26 | margin-block: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/assets/common.js:
--------------------------------------------------------------------------------
1 | ( ( element, i18n, apiFetch ) => {
2 | // (Global) object; holds some helper functions.
3 | window.IndieBlocks = {
4 | hCite: ( className, attributes, innerBlocks = null ) => {
5 | const { createElement: el, createInterpolateElement: interpolate } = element;
6 | const { __, sprintf } = i18n;
7 |
8 | const messagesWithByline = {
9 | /* translators: %1$s: Link to the bookmarked page. %2$s: Author of the bookmarked page. */
10 | 'u-bookmark-of': __( 'Bookmarked %1$s by %2$s.', 'indieblocks' ),
11 | /* translators: %1$s: Link to the "liked" page. %2$s: Author of the "liked" page. */
12 | 'u-like-of': __( 'Likes %1$s by %2$s.', 'indieblocks' ),
13 | /* translators: %1$s: Link to the page being replied to. %2$s: Author of the page being replied to. */
14 | 'u-in-reply-to': __( 'In reply to %1$s by %2$s.', 'indieblocks' ),
15 | /* translators: %1$s: Link to the "page" being reposted. %2$s: Author of the "page" being reposted. */
16 | 'u-repost-of': __( 'Reposted %1$s by %2$s.', 'indieblocks' ),
17 | };
18 |
19 | const messages = {
20 | /* translators: %s: Link to the bookmarked page. */
21 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ),
22 | /* translators: %s: Link to the "liked" page. */
23 | 'u-like-of': __( 'Likes %s.', 'indieblocks' ),
24 | /* translators: %s: Link to the page being replied to. */
25 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ),
26 | /* translators: %s: Link to the "page" being reposted. */
27 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ),
28 | };
29 |
30 | const message =
31 | ! attributes.author || 'undefined' === attributes.author
32 | ? messages[ className ]
33 | : messagesWithByline[ className ];
34 |
35 | const name = attributes.title || attributes.url;
36 |
37 | return el(
38 | 'div',
39 | { className: className + ' h-cite' },
40 | el(
41 | 'p',
42 | {}, // Adding paragraphs this time around.
43 | el(
44 | 'i',
45 | {}, // Could've been `span`, with a `className` or something, but works well enough.
46 | ! attributes.author || 'undefined' === attributes.author
47 | ? interpolate( sprintf( message, '' + name + '' ), {
48 | a: el( 'a', {
49 | className:
50 | attributes.title && attributes.url !== attributes.title
51 | ? 'u-url p-name' // No title means no `p-name`.
52 | : 'u-url',
53 | href: attributes.url,
54 | } ),
55 | } )
56 | : interpolate(
57 | sprintf( message, '' + name + '', '' + attributes.author + '' ),
58 | {
59 | a: el( 'a', {
60 | className:
61 | attributes.title && attributes.url !== attributes.title
62 | ? 'u-url p-name'
63 | : 'u-url',
64 | href: attributes.url,
65 | } ),
66 | span: el( 'span', {
67 | className: 'p-author',
68 | } ),
69 | }
70 | )
71 | )
72 | ),
73 | 'u-repost-of' === className && innerBlocks && ! attributes.empty
74 | ? el( 'blockquote', { className: 'wp-block-quote e-content' }, el( innerBlocks ) )
75 | : null
76 | );
77 | },
78 | /**
79 | * Calls a backend function that parses a URL for microformats and the like,
80 | * and sets attributes accordingly.
81 | */
82 | updateMeta: ( props ) => {
83 | const url = props.attributes.url;
84 |
85 | if ( props.attributes.customTitle && props.attributes.customAuthor ) {
86 | // We're using custom values for both title and author; nothing
87 | // to do here.
88 | return;
89 | }
90 |
91 | if ( ! IndieBlocks.isValidUrl( url ) ) {
92 | return;
93 | }
94 |
95 | // Like a time-out.
96 | const controller = new AbortController();
97 | const timeoutId = setTimeout( () => {
98 | controller.abort();
99 | }, 6000 );
100 |
101 | apiFetch( {
102 | path: '/indieblocks/v1/meta?url=' + encodeURIComponent( url ),
103 | signal: controller.signal, // That time-out thingy.
104 | } )
105 | .then( ( response ) => {
106 | if ( ! props.attributes.customTitle && ( response.name || '' === response.name ) ) {
107 | // Got a, possibly empty, title.
108 | props.setAttributes( { title: response.name } );
109 | }
110 |
111 | if ( ! props.attributes.customAuthor && ( response.author.name || '' === response.author.name ) ) {
112 | // Got a, possibly empty, name.
113 | props.setAttributes( { author: response.author.name } );
114 | }
115 |
116 | clearTimeout( timeoutId );
117 | } )
118 | .catch( ( error ) => {
119 | // The request timed out or otherwise failed. Leave as is.
120 | } );
121 | },
122 | /**
123 | * Validates a URL.
124 | */
125 | isValidUrl: ( string ) => {
126 | try {
127 | new URL( string );
128 | } catch ( error ) {
129 | return false;
130 | }
131 |
132 | return true;
133 | },
134 | };
135 | } )( window.wp.element, window.wp.i18n, window.wp.apiFetch );
136 |
--------------------------------------------------------------------------------
/assets/location.css:
--------------------------------------------------------------------------------
1 | .indieblocks-location__fetch-button {
2 | height: 32px;
3 | }
4 |
5 | @media screen and (max-width: 782px) {
6 | .indieblocks-location__fetch-button {
7 | min-height: 40px;
8 | }
9 | }
10 |
11 | .indieblocks-location__address-field {
12 | width: 100%;
13 | }
14 |
--------------------------------------------------------------------------------
/assets/location.js:
--------------------------------------------------------------------------------
1 | ( ( element, components, i18n, data, coreData, apiFetch, plugins, editor ) => {
2 | const { createElement: el, useEffect, useState, useRef } = element;
3 | const { Button, Flex, FlexBlock, FlexItem, TextControl, PanelRow } = components;
4 | const { __ } = i18n;
5 | const { useSelect } = data;
6 | const { useEntityProp } = coreData;
7 | const { registerPlugin } = plugins;
8 | const { PluginDocumentSettingPanel } = editor;
9 |
10 | // @link https://wordpress.stackexchange.com/questions/362975/admin-notification-after-save-post-when-ajax-saving-in-gutenberg
11 | const doneSaving = () => {
12 | const { isSaving, isAutosaving, status } = useSelect( ( select ) => {
13 | return {
14 | isSaving: select( 'core/editor' ).isSavingPost(),
15 | isAutosaving: select( 'core/editor' ).isAutosavingPost(),
16 | status: select( 'core/editor' ).getEditedPostAttribute( 'status' ),
17 | };
18 | } );
19 |
20 | const [ wasSaving, setWasSaving ] = useState( isSaving && ! isAutosaving ); // Ignore autosaves.
21 |
22 | if ( wasSaving ) {
23 | if ( ! isSaving ) {
24 | setWasSaving( false );
25 | return true;
26 | }
27 | } else if ( isSaving && ! isAutosaving ) {
28 | setWasSaving( true );
29 | }
30 |
31 | return false;
32 | };
33 |
34 | registerPlugin( 'indieblocks-location-panel', {
35 | render: ( props ) => {
36 | const { postId, postType } = useSelect( ( select ) => {
37 | return {
38 | postId: select( 'core/editor' ).getCurrentPostId(),
39 | postType: select( 'core/editor' ).getCurrentPostType(),
40 | };
41 | }, [] );
42 |
43 | const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
44 |
45 | const latitude = meta?.geo_latitude ?? '';
46 | const longitude = meta?.geo_longitude ?? '';
47 | const geoAddress = meta?.geo_address ?? '';
48 |
49 | const stateRef = useRef();
50 | stateRef.current = meta;
51 |
52 | // This seems superfluous, but let's leave it in place, in case our
53 | // server's "too slow" to immediately return a location name.
54 |
55 | // The idea here was to fetch a location name after it is set in the
56 | // background. So that if the OpenStreetMap geolocation API is "too slow"
57 | // we'll still get an "up-to-date" location.
58 | const fetchLocation = () => {
59 | if ( ! postId ) {
60 | return;
61 | }
62 |
63 | if ( ! postType ) {
64 | return;
65 | }
66 |
67 | if ( stateRef.current.geo_address ) {
68 | return;
69 | }
70 |
71 | // Like a time-out.
72 | const controller = new AbortController();
73 | const timeoutId = setTimeout( () => {
74 | controller.abort();
75 | }, 6000 );
76 |
77 | apiFetch( {
78 | path: '/wp/v2/' + postType + '/' + postId,
79 | signal: controller.signal, // That time-out thingy.
80 | } )
81 | .then( ( response ) => {
82 | clearTimeout( timeoutId );
83 |
84 | if ( response.indieblocks_location && response.indieblocks_location.geo_address ) {
85 | // This function does not do anything besides displaying a location name.
86 | setMeta( {
87 | ...stateRef.current,
88 | geo_address: response.indieblocks_location.geo_address,
89 | } );
90 | }
91 | } )
92 | .catch( ( error ) => {
93 | // The request timed out or otherwise failed. Leave as is.
94 | } );
95 | };
96 |
97 | if ( doneSaving() && ! geoAddress ) {
98 | setTimeout( () => {
99 | fetchLocation();
100 | }, 1500 );
101 |
102 | setTimeout( () => {
103 | fetchLocation();
104 | }, 15000 );
105 | }
106 |
107 | // `navigator.geolocation.getCurrentPosition` callback.
108 | const updatePosition = ( position ) => {
109 | setMeta( {
110 | ...stateRef.current,
111 | geo_latitude: position.coords.latitude.toString(),
112 | geo_longitude: position.coords.longitude.toString(),
113 | } );
114 | };
115 |
116 | // Runs once.
117 | useEffect( () => {
118 | if ( '' !== latitude ) {
119 | return;
120 | }
121 |
122 | if ( '' !== longitude ) {
123 | return;
124 | }
125 |
126 | const shouldUpdate = indieblocks_location_obj?.should_update ?? '0';
127 | if ( '1' !== shouldUpdate ) {
128 | return;
129 | }
130 |
131 | if ( ! navigator.geolocation ) {
132 | return;
133 | }
134 |
135 | navigator.geolocation.getCurrentPosition( updatePosition, ( error ) => {
136 | // Do nothing.
137 | console.log( error );
138 | } );
139 | }, [ updatePosition ] );
140 |
141 | return el(
142 | PluginDocumentSettingPanel,
143 | {
144 | name: 'indieblocks-location-panel',
145 | title: __( 'Location', 'indieblocks' ),
146 | },
147 | el(
148 | PanelRow,
149 | {},
150 | el(
151 | Flex,
152 | { align: 'end' },
153 | el(
154 | FlexBlock,
155 | {},
156 | el( TextControl, {
157 | label: __( 'Latitude', 'indieblocks' ),
158 | value: latitude,
159 | onChange: ( value ) => {
160 | setMeta( { ...meta, geo_latitude: value } );
161 | },
162 | } )
163 | ),
164 | el(
165 | FlexBlock,
166 | {},
167 | el( TextControl, {
168 | label: __( 'Longitude', 'indieblocks' ),
169 | value: longitude,
170 | onChange: ( value ) => {
171 | setMeta( {
172 | ...meta,
173 | geo_longitude: value,
174 | } );
175 | },
176 | } )
177 | ),
178 | el(
179 | FlexItem,
180 | {},
181 | el(
182 | Button,
183 | {
184 | className: 'indieblocks-location__fetch-button',
185 | onClick: () => {
186 | if ( ! navigator.geolocation ) {
187 | return;
188 | }
189 |
190 | navigator.geolocation.getCurrentPosition( updatePosition, ( error ) => {
191 | // Do nothing.
192 | console.log( error );
193 | } );
194 | },
195 | variant: 'secondary',
196 | },
197 | __( 'Fetch', 'indieblocks' )
198 | )
199 | )
200 | )
201 | ),
202 | el(
203 | PanelRow,
204 | {},
205 | // To allow authors to manually override or pass on a location.
206 | el( TextControl, {
207 | className: 'indieblocks-location__address-field',
208 | label: __( 'Location', 'indieblocks' ),
209 | value: geoAddress,
210 | onChange: ( value ) => {
211 | setMeta( { ...meta, geo_address: value } );
212 | },
213 | } )
214 | )
215 | );
216 | },
217 | } );
218 | } )(
219 | window.wp.element,
220 | window.wp.components,
221 | window.wp.i18n,
222 | window.wp.data,
223 | window.wp.coreData,
224 | window.wp.apiFetch,
225 | window.wp.plugins,
226 | window.wp.editor
227 | );
228 |
--------------------------------------------------------------------------------
/assets/mm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janboddez/indieblocks/187985d5c505f4f5041dc391d498cd7f021e0eca/assets/mm.png
--------------------------------------------------------------------------------
/assets/remove-extra-margin.css:
--------------------------------------------------------------------------------
1 | .wp-block-post-title.screen-reader-text + * {
2 | margin-block-start: 0 !important;
3 | }
4 |
--------------------------------------------------------------------------------
/assets/webmention-icons.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/assets/webmention-legacy.js:
--------------------------------------------------------------------------------
1 | jQuery( document ).ready( function ( $ ) {
2 | $( '#indieblocks-webmention .indieblocks-resend-webmention' ).click( function () {
3 | var button = $( this );
4 | var type = button.data( 'type' );
5 | var data = {
6 | 'action': 'indieblocks_resend_webmention',
7 | 'type': type, // Post or comment.
8 | 'obj_id': 'post' === type ? $( '[name="post_ID"]' ).val() : $( '[name="comment_ID"]' ).val(), // Current post or comment ID.
9 | '_wp_nonce': button.data( 'nonce' ), // Nonce.
10 | };
11 |
12 | $.post( ajaxurl, data, function ( response ) {
13 | button.parent().find( 'p' ).remove();
14 | button.parent().append( '
' + indieblocks_webmention_legacy_obj.message + '
' );
15 | button.remove();
16 | } );
17 | } );
18 |
19 | $( '.indieblocks-delete-avatar' ).click( function () {
20 | var button = $( this );
21 | var data = {
22 | 'action': 'indieblocks_delete_avatar',
23 | 'comment_id': $( '[name="comment_ID"]' ).val(), // Current comment ID.
24 | '_wp_nonce': button.data( 'nonce' ), // Nonce.
25 | };
26 |
27 | $.post( ajaxurl, data, function ( response ) {
28 | // Bit lazy, but 'kay.
29 | location.reload();
30 | } );
31 | } );
32 | } );
33 |
--------------------------------------------------------------------------------
/assets/webmention.js:
--------------------------------------------------------------------------------
1 | ( ( element, components, i18n, data, coreData, plugins, editPost ) => {
2 | const { createElement: el, createInterpolateElement: interpolate, useState } = element;
3 | const { Button, PanelBody } = components;
4 | const { __, sprintf } = i18n;
5 | const { useSelect } = data;
6 | const { useEntityRecord } = coreData;
7 | const { registerPlugin } = plugins;
8 | const { PluginSidebar, PluginSidebarMoreMenuItem } = editPost;
9 |
10 | const reschedule = ( postId, setWebmention ) => {
11 | if ( ! postId ) {
12 | return false;
13 | }
14 |
15 | // Like a time-out.
16 | const controller = new AbortController();
17 | const timeoutId = setTimeout( () => {
18 | controller.abort();
19 | }, 6000 );
20 |
21 | try {
22 | fetch( indieblocks_webmention_obj.ajaxurl, {
23 | signal: controller.signal, // That time-out thingy.
24 | method: 'POST',
25 | body: new URLSearchParams( {
26 | action: 'indieblocks_resend_webmention',
27 | type: 'post',
28 | obj_id: postId,
29 | _wp_nonce: indieblocks_webmention_obj.nonce,
30 | } ),
31 | } )
32 | .then( ( response ) => {
33 | clearTimeout( timeoutId );
34 | setWebmention( 'scheduled' ); // So as to trigger a re-render.
35 | } )
36 | .catch( ( error ) => {
37 | // The request timed out or otherwise failed. Leave as is.
38 | throw new Error( 'The "Resend" request failed.' );
39 | } );
40 | } catch ( error ) {
41 | return false;
42 | }
43 |
44 | return true;
45 | };
46 |
47 | if ( '1' === indieblocks_webmention_obj.show_meta_box ) {
48 | // Gutenberg sidebar.
49 | registerPlugin( 'indieblocks-webmention-sidebar', {
50 | render: () => {
51 | const { postId, postType } = useSelect( ( select ) => {
52 | return {
53 | postId: select( 'core/editor' ).getCurrentPostId(),
54 | postType: select( 'core/editor' ).getCurrentPostType(),
55 | };
56 | }, [] );
57 |
58 | const { record, isResolving } = useEntityRecord( 'postType', postType, postId );
59 | const [ webmention, setWebmention ] = useState( record?.indieblocks_webmention ?? null );
60 |
61 | let output = [];
62 |
63 | // console.table( webmention );
64 |
65 | if ( typeof webmention === 'object' ) {
66 | Object.keys( webmention ).forEach( ( key ) => {
67 | const value = webmention[ key ];
68 |
69 | if ( ! value.endpoint ) {
70 | return;
71 | }
72 |
73 | let line = '';
74 |
75 | if ( value.sent ) {
76 | line = sprintf(
77 | /* translators: %1$s: Webmention endpoint. %2$s: HTTP response code. */
78 | __( 'Sent to %1$s: %2$d.', 'indieblocks' ),
79 | '' + value.endpoint + '',
80 | value.code
81 | );
82 | line = el(
83 | 'p',
84 | {},
85 | interpolate( line, {
86 | a: el( 'a', {
87 | href: encodeURI( value.endpoint ),
88 | title: value.sent,
89 | target: '_blank',
90 | rel: 'noreferrer noopener',
91 | } ),
92 | } )
93 | );
94 | output.push( line );
95 | } else if ( value.retries ) {
96 | if ( value.retries >= 3 ) {
97 | line = sprintf(
98 | __( 'Could not send webmention to %s.', 'indieblocks' ),
99 | value.endpoint
100 | );
101 | line = el(
102 | 'p',
103 | {},
104 | interpolate( line, {
105 | a: el( 'a', {
106 | href: encodeURI( value.endpoint ),
107 | target: '_blank',
108 | rel: 'noreferrer noopener',
109 | } ),
110 | } )
111 | );
112 | output.push( line );
113 | } else {
114 | line = sprintf(
115 | __( 'Could not send webmention to %s. Trying again soon.', 'indieblocks' ),
116 | value.endpoint
117 | );
118 | line = el(
119 | 'p',
120 | {},
121 | interpolate( line, {
122 | a: el( 'a', {
123 | href: encodeURI( value.endpoint ),
124 | target: '_blank',
125 | rel: 'noreferrer noopener',
126 | } ),
127 | } )
128 | );
129 | output.push( line );
130 | }
131 | }
132 | } );
133 | } else if ( 'scheduled' === webmention ) {
134 | line = el( 'p', {}, __( 'Webmention scheduled.', 'indieblocks' ) );
135 | output.push( line );
136 | }
137 |
138 | if ( ! output.length ) {
139 | // return;
140 | output.push( el( 'p', {}, __( 'No endpoints found.', 'indieblocks' ) ) );
141 | }
142 |
143 | return [
144 | el(
145 | PluginSidebarMoreMenuItem,
146 | { target: 'indieblocks-webmention-sidebar' },
147 | __( 'Webmention', 'indieblocks' )
148 | ),
149 | el(
150 | PluginSidebar,
151 | {
152 | icon: el(
153 | 'svg',
154 | {
155 | xmlns: 'http://www.w3.org/2000/svg',
156 | viewBox: '0 0 24 24',
157 | },
158 | el( 'path', {
159 | d: 'm13.91 18.089-1.894-5.792h-.032l-1.863 5.792H7.633L4.674 6.905h2.458L8.9 14.518h.032l1.94-5.793h2.302l1.91 5.886h.031L16.48 8.73l-1.778-.004L18.387 5.3l2.287 3.43-1.81-.001-2.513 9.36z',
160 | } )
161 | ),
162 | name: 'indieblocks-webmention-sidebar',
163 | title: __( 'Webmention', 'indieblocks' ),
164 | },
165 | el(
166 | PanelBody,
167 | {},
168 | output,
169 | el(
170 | 'p',
171 | {},
172 | el(
173 | Button,
174 | {
175 | onClick: () => {
176 | if ( confirm( __( 'Reschedule webmentions?', 'indieblocks' ) ) ) {
177 | reschedule( postId, setWebmention );
178 | }
179 | },
180 | variant: 'secondary',
181 | },
182 | __( 'Resend', 'indieblocks' )
183 | )
184 | )
185 | )
186 | ),
187 | ];
188 | },
189 | } );
190 | }
191 | } )(
192 | window.wp.element,
193 | window.wp.components,
194 | window.wp.i18n,
195 | window.wp.data,
196 | window.wp.coreData,
197 | window.wp.plugins,
198 | window.wp.editPost
199 | );
200 |
--------------------------------------------------------------------------------
/blocks/bookmark/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-i18n',
11 | 'indieblocks-common',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/bookmark/block.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-bookmark > .h-cite:first-child > p {
2 | margin-block: 0;
3 | }
4 |
5 | .wp-block-indieblocks-bookmark > .block-editor-inner-blocks {
6 | margin-block: 1em;
7 | min-height: 35px;
8 | }
9 |
--------------------------------------------------------------------------------
/blocks/bookmark/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, data, i18n, IndieBlocks ) => {
2 | const { createBlock, registerBlockType } = blocks;
3 | const { createElement: el, renderToString, useEffect } = element;
4 | const { BlockControls, InnerBlocks, InspectorControls, useBlockProps } = blockEditor;
5 | const { PanelBody, Placeholder, ToggleControl, TextControl } = components;
6 | const { useSelect } = data;
7 | const { __ } = i18n;
8 |
9 | registerBlockType( 'indieblocks/bookmark', {
10 | icon: el(
11 | 'svg',
12 | {
13 | xmlns: 'http://www.w3.org/2000/svg',
14 | viewBox: '0 0 24 24',
15 | },
16 | el( 'path', {
17 | d: 'M8.1 5a2 2 0 0 0-2 2v12.1L12 15l5.9 4.1V7a2 2 0 0 0-2-2H8.1z',
18 | } )
19 | ),
20 | edit: ( props ) => {
21 | const url = props.attributes.url;
22 | const customTitle = props.attributes.customTitle;
23 | const title = props.attributes.title || ''; // May not be present in the saved HTML, so we need a fallback value even when `block.json` contains a default.
24 | const customAuthor = props.attributes.customAuthor;
25 | const author = props.attributes.author || '';
26 |
27 | const updateEmpty = ( empty ) => {
28 | props.setAttributes( { empty } );
29 | };
30 |
31 | const { parentClientId, innerBlocks } = useSelect( ( select ) => {
32 | const parentClientId = select( 'core/block-editor' ).getBlockHierarchyRootClientId( props.clientId );
33 |
34 | return {
35 | parentClientId: parentClientId,
36 | innerBlocks: select( 'core/block-editor' ).getBlocks( parentClientId ),
37 | };
38 | }, [] );
39 |
40 | // To determine whether `.e-content` and `InnerBlocks.Content`
41 | // should be saved (and echoed).
42 | useEffect( () => {
43 | let empty = true;
44 |
45 | if ( innerBlocks.length > 1 ) {
46 | // More than one child block.
47 | empty = false;
48 | }
49 |
50 | if (
51 | 'undefined' !== typeof innerBlocks[ 0 ] &&
52 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.content &&
53 | innerBlocks[ 0 ].attributes.content.length
54 | ) {
55 | // A non-empty paragraph or heading. Empty paragraphs are
56 | // almost unavoidable, so it's important to get this right.
57 | empty = false;
58 | }
59 |
60 | if (
61 | 'undefined' !== typeof innerBlocks[ 0 ] &&
62 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.href &&
63 | innerBlocks[ 0 ].attributes.href.length
64 | ) {
65 | // A non-empty image.
66 | empty = false;
67 | }
68 |
69 | if ( 'undefined' !== typeof innerBlocks[ 0 ] && innerBlocks[ 0 ].innerBlocks.length ) {
70 | // A quote or gallery, empty or not.
71 | empty = false;
72 | }
73 |
74 | updateEmpty( empty );
75 | }, [ innerBlocks, updateEmpty ] );
76 |
77 | const placeholderProps = {
78 | icon: el(
79 | 'svg',
80 | {
81 | xmlns: 'http://www.w3.org/2000/svg',
82 | viewBox: '0 0 24 24',
83 | },
84 | el( 'path', {
85 | d: 'M8.1 5a2 2 0 0 0-2 2v12.1L12 15l5.9 4.1V7a2 2 0 0 0-2-2H8.1z',
86 | } )
87 | ),
88 | label: __( 'Bookmark', 'indieblocks' ),
89 | isColumnLayout: true,
90 | };
91 |
92 | if ( ! url || 'undefined' === url ) {
93 | placeholderProps.instructions = __(
94 | 'Add a URL and have WordPress automatically generate a correctly microformatted introductory paragraph.',
95 | 'indieblocks'
96 | );
97 | }
98 |
99 | const titleProps = {
100 | label: __( 'Title', 'indieblocks' ),
101 | value: title,
102 | onChange: ( value ) => {
103 | props.setAttributes( { title: value } );
104 | },
105 | };
106 |
107 | if ( ! customTitle ) {
108 | titleProps.readOnly = 'readonly';
109 | }
110 |
111 | const authorProps = {
112 | label: __( 'Author', 'indieblocks' ),
113 | value: author,
114 | onChange: ( value ) => {
115 | props.setAttributes( { author: value } );
116 | },
117 | };
118 |
119 | if ( ! customAuthor ) {
120 | authorProps.readOnly = 'readonly';
121 | }
122 |
123 | return el(
124 | 'div',
125 | useBlockProps(),
126 | el( BlockControls ),
127 | props.isSelected || ! url || 'undefined' === url
128 | ? el(
129 | Placeholder,
130 | placeholderProps,
131 | el(
132 | InspectorControls,
133 | { key: 'inspector' },
134 | el(
135 | PanelBody,
136 | {
137 | title: __( 'Title and Author' ),
138 | initialOpen: true,
139 | },
140 | el( TextControl, titleProps ),
141 | el( ToggleControl, {
142 | label: __( 'Customize title', 'indieblocks' ),
143 | checked: customTitle,
144 | onChange: ( value ) => {
145 | props.setAttributes( {
146 | customTitle: value,
147 | } );
148 | },
149 | } ),
150 | el( TextControl, authorProps ),
151 | el( ToggleControl, {
152 | label: __( 'Customize author', 'indieblocks' ),
153 | checked: customAuthor,
154 | onChange: ( value ) => {
155 | props.setAttributes( {
156 | customAuthor: value,
157 | } );
158 | },
159 | } )
160 | )
161 | ),
162 | el( TextControl, {
163 | label: __( 'URL', 'indieblocks' ),
164 | value: url,
165 | onChange: ( value ) => {
166 | props.setAttributes( { url: value } );
167 | },
168 | onKeyDown: ( event ) => {
169 | if ( 13 === event.keyCode ) {
170 | IndieBlocks.updateMeta( props );
171 | }
172 | },
173 | onBlur: () => {
174 | IndieBlocks.updateMeta( props );
175 | },
176 | } )
177 | )
178 | : IndieBlocks.hCite( 'u-bookmark-of', props.attributes ),
179 | el( InnerBlocks, {
180 | template: [ [ 'core/paragraph' ] ],
181 | templateLock: false,
182 | } ) // Always **show** (editable) `InnerBlocks`.
183 | );
184 | },
185 | save: ( props ) =>
186 | el(
187 | 'div',
188 | useBlockProps.save(),
189 | ! props.attributes.url || 'undefined' === props.attributes.url
190 | ? null // Can't do much without a URL.
191 | : IndieBlocks.hCite( 'u-bookmark-of', props.attributes ),
192 | ! props.attributes.empty ? el( 'div', { className: 'e-content' }, el( InnerBlocks.Content ) ) : null
193 | ),
194 | transforms: {
195 | from: [
196 | {
197 | type: 'block',
198 | blocks: [ 'indieblocks/context' ],
199 | transform: ( { url } ) => {
200 | return createBlock( 'indieblocks/bookmark', { url } );
201 | },
202 | },
203 | ],
204 | to: [
205 | {
206 | type: 'block',
207 | blocks: [ 'core/group' ],
208 | transform: ( attributes, innerBlocks ) => {
209 | return createBlock( 'core/group', attributes, [
210 | createBlock( 'core/html', {
211 | content: renderToString( IndieBlocks.hCite( 'u-bookmark-of', attributes ) ),
212 | } ),
213 | createBlock( 'core/group', { className: 'e-content' }, innerBlocks ),
214 | ] );
215 | },
216 | },
217 | {
218 | type: 'block',
219 | blocks: [ 'indieblocks/like' ],
220 | transform: ( attributes, innerBlocks ) => {
221 | return createBlock( 'indieblocks/like', attributes, innerBlocks );
222 | },
223 | },
224 | ],
225 | },
226 | } );
227 | } )(
228 | window.wp.blocks,
229 | window.wp.element,
230 | window.wp.blockEditor,
231 | window.wp.components,
232 | window.wp.data,
233 | window.wp.i18n,
234 | window.IndieBlocks
235 | );
236 |
--------------------------------------------------------------------------------
/blocks/bookmark/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Bookmark",
4 | "description": "Bookmark and annotate web pages or posts.",
5 | "name": "indieblocks/bookmark",
6 | "category": "text",
7 | "editorScript": "file:./block.js",
8 | "style": "file:./block.css",
9 | "attributes": {
10 | "url": {
11 | "type": "string",
12 | "source": "attribute",
13 | "selector": ".u-url",
14 | "attribute": "href",
15 | "default": ""
16 | },
17 | "customTitle": {
18 | "type": "boolean",
19 | "default": false
20 | },
21 | "title": {
22 | "type": "string",
23 | "source": "text",
24 | "selector": ".p-name",
25 | "default": ""
26 | },
27 | "customAuthor": {
28 | "type": "boolean",
29 | "default": false
30 | },
31 | "author": {
32 | "type": "string",
33 | "source": "text",
34 | "selector": ".p-author",
35 | "default": ""
36 | },
37 | "empty": {
38 | "type": "boolean",
39 | "default": true
40 | }
41 | },
42 | "example": {
43 | "attributes": {
44 | "url": "https://example.org/",
45 | "title": "Example Domain",
46 | "author": "Alice"
47 | }
48 | },
49 | "textdomain": "indieblocks"
50 | }
51 |
--------------------------------------------------------------------------------
/blocks/context/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-i18n',
11 | ),
12 | );
13 |
--------------------------------------------------------------------------------
/blocks/context/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, i18n ) => {
2 | const { createBlock, getSaveContent, registerBlockType } = blocks;
3 | const { createElement: el, createInterpolateElement: interpolate } = element;
4 | const { BlockControls, useBlockProps } = blockEditor;
5 | const { Placeholder, RadioControl, TextControl } = components;
6 | const { __, sprintf } = i18n;
7 |
8 | const messages = {
9 | /* translators: %s: Link to the bookmarked page. */
10 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ),
11 | /* translators: %s: Link to the "liked" page. */
12 | 'u-like-of': __( 'Likes %s.', 'indieblocks' ),
13 | /* translators: %s: Link to the page being replied to. */
14 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ),
15 | /* translators: %s: Link to the "page" being reposted. */
16 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ),
17 | };
18 |
19 | const render = ( blockProps, url, kind ) => {
20 | return el(
21 | 'div',
22 | blockProps,
23 | ! url || 'undefined' === url
24 | ? null // Return nothing.
25 | : el(
26 | 'i',
27 | {},
28 | interpolate( sprintf( messages[ kind ], '' + encodeURI( url ) + '' ), {
29 | a: el( 'a', { className: kind, href: encodeURI( url ) } ),
30 | } )
31 | )
32 | );
33 | };
34 |
35 | registerBlockType( 'indieblocks/context', {
36 | edit: ( props ) => {
37 | const url = props.attributes.url;
38 | const kind = props.attributes.kind;
39 |
40 | const placeholderProps = {
41 | icon: 'format-status',
42 | label: __( 'Context', 'indieblocks' ),
43 | isColumnLayout: true,
44 | };
45 |
46 | if ( ! url || 'undefined' === url ) {
47 | placeholderProps.instructions = __(
48 | 'Add a URL and post type, and have WordPress automatically generate a correctly microformatted introductory paragraph.',
49 | 'indieblocks'
50 | );
51 | }
52 |
53 | return el(
54 | 'div',
55 | useBlockProps(),
56 | el( BlockControls ),
57 | props.isSelected || ! url || 'undefined' === url
58 | ? el( Placeholder, placeholderProps, [
59 | el( TextControl, {
60 | label: __( 'URL', 'indieblocks' ),
61 | value: url,
62 | onChange: ( value ) => {
63 | props.setAttributes( { url: value } );
64 | },
65 | } ),
66 | el( RadioControl, {
67 | label: __( 'Type', 'indieblocks' ),
68 | selected: kind,
69 | options: [
70 | { label: __( 'Bookmark', 'indieblocks' ), value: 'u-bookmark-of' },
71 | { label: __( 'Like', 'indieblocks' ), value: 'u-like-of' },
72 | { label: __( 'Reply', 'indieblocks' ), value: 'u-in-reply-to' },
73 | { label: __( 'Repost', 'indieblocks' ), value: 'u-repost-of' },
74 | ],
75 | onChange: ( value ) => {
76 | props.setAttributes( { kind: value } );
77 | },
78 | } ),
79 | ] )
80 | : render( {}, url, kind )
81 | );
82 | },
83 | save: ( props ) => {
84 | return render( useBlockProps.save(), props.attributes.url, props.attributes.kind );
85 | },
86 | deprecated: [
87 | {
88 | save: ( props ) => {
89 | const url = props.attributes.url;
90 | const kind = props.attributes.kind;
91 |
92 | const messages = {
93 | /* translators: %s: Link to the bookmarked page. */
94 | 'u-bookmark-of': __( 'Bookmarked %s.', 'indieblocks' ),
95 | /* translators: %s: Link to the "liked" page. */
96 | 'u-like-of': __( 'Liked %s.', 'indieblocks' ),
97 | /* translators: %s: Link to the page being replied to. */
98 | 'u-in-reply-to': __( 'In reply to %s.', 'indieblocks' ),
99 | /* translators: %s: Link to the "page" being reposted. */
100 | 'u-repost-of': __( 'Reposted %s.', 'indieblocks' ),
101 | };
102 |
103 | return el(
104 | 'div',
105 | useBlockProps.save(),
106 | el(
107 | 'i',
108 | {},
109 | interpolate( sprintf( messages[ kind ], '' + url + '' ), {
110 | a: el( 'a', { className: kind, href: url } ),
111 | } )
112 | )
113 | );
114 | },
115 | },
116 | ],
117 | transforms: {
118 | to: [
119 | {
120 | type: 'block',
121 | blocks: [ 'core/html' ],
122 | transform: ( attributes ) => {
123 | return createBlock( 'core/html', {
124 | content: getSaveContent( 'indieblocks/context', attributes ),
125 | } );
126 | },
127 | },
128 | ],
129 | },
130 | } );
131 | } )( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.components, window.wp.i18n );
132 |
--------------------------------------------------------------------------------
/blocks/context/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Context",
4 | "name": "indieblocks/context",
5 | "category": "text",
6 | "icon": "format-status",
7 | "editorScript": "file:./block.js",
8 | "editorStyle": "file:./editor.css",
9 | "attributes": {
10 | "url": {
11 | "type": "string",
12 | "source": "attribute",
13 | "selector": "a",
14 | "attribute": "href",
15 | "default": ""
16 | },
17 | "kind": {
18 | "type": "string",
19 | "source": "attribute",
20 | "selector": "a",
21 | "attribute": "class",
22 | "default": "u-bookmark-of"
23 | }
24 | },
25 | "example": {
26 | "attributes": {
27 | "url": "https://example.org/",
28 | "kind": "u-bookmark-of"
29 | }
30 | },
31 | "textdomain": "indieblocks"
32 | }
33 |
--------------------------------------------------------------------------------
/blocks/context/editor.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-context .components-checkbox-control__input[type="checkbox"],
2 | .wp-block-indieblocks-context .components-radio-control__input[type="radio"] {
3 | /* Restore "default" values, should themes want to change these. */
4 | height: 24px;
5 | width: 24px;
6 | }
7 |
8 | @media (min-width: 600px) {
9 | .wp-block-indieblocks-context .components-checkbox-control__input[type="checkbox"],
10 | .wp-block-indieblocks-context .components-radio-control__input[type="radio"] {
11 | height: 20px;
12 | width: 20px;
13 | }
14 | }
15 |
16 | .wp-block-indieblocks-context .components-text-control__input[readonly] {
17 | background-color: rgb(240, 240, 241);
18 | }
19 |
--------------------------------------------------------------------------------
/blocks/facepile-content/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-i18n',
10 | 'wp-components',
11 | 'indieblocks-common',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/facepile-content/block.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --indieblocks-avatar-size: 36px;
3 | }
4 |
5 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-1 {
6 | --indieblocks-avatar-size: 24px;
7 | }
8 |
9 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-2 {
10 | --indieblocks-avatar-size: 36px;
11 | }
12 |
13 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-3 {
14 | --indieblocks-avatar-size: 48px;
15 | }
16 |
17 | .wp-block-indieblocks-facepile-content .indieblocks-avatar-size-4 {
18 | --indieblocks-avatar-size: 60px;
19 | }
20 |
21 | .wp-block-indieblocks-facepile-content ul {
22 | list-style: none;
23 | display: flex;
24 | flex-wrap: wrap;
25 | margin: 0;
26 | padding: 0;
27 | padding-inline-end: var(--indieblocks-avatar-size);
28 | }
29 |
30 | .wp-block-indieblocks-facepile-content li {
31 | display: inline-block;
32 | margin-inline-end: calc(-0.33 * var(--indieblocks-avatar-size));
33 | position: relative;
34 | }
35 |
36 | .wp-block-indieblocks-facepile-content li:not(:last-child):hover {
37 | margin-inline-end: calc(-0.05 * var(--indieblocks-avatar-size));
38 | }
39 |
40 | .wp-block-indieblocks-facepile-content a {
41 | display: block;
42 | position: relative;
43 | }
44 |
45 | .wp-block-indieblocks-facepile-content .avatar {
46 | display: block;
47 | width: var(--indieblocks-avatar-size);
48 | height: var(--indieblocks-avatar-size);
49 | border-radius: 50%;
50 | }
51 |
52 | .wp-block-indieblocks-facepile-content .icon {
53 | display: block;
54 | width: calc(0.5 * var(--indieblocks-avatar-size));
55 | height: calc(0.5 * var(--indieblocks-avatar-size));
56 | position: absolute;
57 | right: -3px;
58 | bottom: -3px;
59 | border-radius: 50%;
60 | }
61 |
62 | .wp-block-indieblocks-facepile-content .indieblocks-count .icon {
63 | display: inline;
64 | width: 1.1em;
65 | height: 1.1em;
66 | position: relative;
67 | top: 0.2em;
68 | right: initial;
69 | bottom: initial;
70 | }
71 |
--------------------------------------------------------------------------------
/blocks/facepile-content/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Facepile Content",
4 | "description": "Outputs the actual “facepile” avatars.",
5 | "name": "indieblocks/facepile-content",
6 | "category": "theme",
7 | "editorScript": "file:./block.js",
8 | "style": "file:./block.css",
9 | "render": "file:./render.php",
10 | "attributes": {
11 | "avatarSize": {
12 | "type": "integer",
13 | "default": 2
14 | },
15 | "backgroundColor": {
16 | "type": "string",
17 | "default": "transparent"
18 | },
19 | "icons": {
20 | "type": "boolean",
21 | "default": true
22 | },
23 | "color": {
24 | "type": "string",
25 | "default": "#000"
26 | },
27 | "iconBackgroundColor": {
28 | "type": "string",
29 | "default": "#fff"
30 | },
31 | "type": {
32 | "type": "array",
33 | "default": [ "bookmark", "like", "repost" ]
34 | },
35 | "countOnly": {
36 | "type": "boolean",
37 | "default": false
38 | },
39 | "forceShow": {
40 | "type": "boolean",
41 | "default": false
42 | }
43 | },
44 | "usesContext": [ "postId", "postType" ],
45 | "textdomain": "indieblocks",
46 | "supports": {
47 | "color": {
48 | "gradients": true,
49 | "link": true,
50 | "__experimentalDefaultControls": {
51 | "background": true,
52 | "text": true,
53 | "link": true
54 | }
55 | },
56 | "inserter": false
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/blocks/facepile-content/render.php:
--------------------------------------------------------------------------------
1 | context['postId'] ) ) {
7 | $post_id = $block->context['postId']; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
8 | } elseif ( in_the_loop() ) {
9 | $post_id = get_the_ID(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
10 | }
11 |
12 | if ( empty( $post_id ) ) {
13 | return;
14 | }
15 |
16 | $types = array( 'bookmark', 'like', 'repost' );
17 |
18 | if ( isset( $attributes['type'] ) && is_array( $attributes['type'] ) ) {
19 | $types = $attributes['type'];
20 | }
21 |
22 | if ( empty( $types ) ) {
23 | return;
24 | }
25 |
26 | $facepile_comments = \IndieBlocks\get_facepile_comments( $post_id, $types );
27 |
28 | if ( empty( $facepile_comments ) && empty( $attributes['forceShow'] ) ) {
29 | return;
30 | }
31 |
32 | add_action( 'wp_footer', '\\IndieBlocks\\print_facepile_icons', 999 );
33 |
34 | $output = '';
35 |
36 | if ( ! empty( $attributes['countOnly'] ) ) {
37 | if ( ! empty( $attributes['icons'] ) && ! empty( $attributes['type'] ) ) {
38 | $kind = ( (array) $attributes['type'] )[0];
39 | $output .= ' ' . count( $facepile_comments ) . '
';
40 | } else {
41 | $output .= '' . count( $facepile_comments ) . '
';
42 | }
43 | } else {
44 | // Limit number of avatars shown. Might provide a proper option later.
45 | $facepile_num = apply_filters( 'indieblocks_facepile_num', 25, $post_id );
46 | $facepile_comments = array_slice( $facepile_comments, 0, $facepile_num );
47 |
48 | foreach ( $facepile_comments as $facepile_comment ) {
49 | $avatar = get_avatar( $facepile_comment, 40 );
50 |
51 | if ( empty( $avatar ) ) {
52 | continue;
53 |
54 | // So, normally, WordPress would return a "Mystery Man" or whatever avatar, if there was none. Still, could
55 | // be it doesn't in case there's no author email address.
56 | // $avatar = '
';
57 | }
58 |
59 | $processor = new \WP_HTML_Tag_Processor( $avatar );
60 | $processor->next_tag( 'img' );
61 |
62 | if ( ! empty( $attributes['backgroundColor'] ) ) {
63 | $processor->set_attribute( 'style', 'background:' . esc_attr( $attributes['backgroundColor'] ) ); // Even though `WP_HTML_Tag_Processor::set_attribute()` will run, e.g., `esc_attr()` for us.
64 | }
65 |
66 | $alt = $processor->get_attribute( 'alt' );
67 | $alt = ! empty( $alt ) ? $alt : get_comment_author( $facepile_comment );
68 |
69 | $processor->set_attribute( 'alt', esc_attr( $alt ) );
70 |
71 | $avatar = $processor->get_updated_html();
72 |
73 | $source = get_comment_meta( $facepile_comment->comment_ID, 'indieblocks_webmention_source', true );
74 | $kind = get_comment_meta( $facepile_comment->comment_ID, 'indieblocks_webmention_kind', true );
75 |
76 | if ( in_array( $facepile_comment->comment_type, array( 'bookmark', 'like', 'repost' ), true ) ) {
77 | // Mentions initiated by the Webmention plugin use a slightly different data structure.
78 | $source = get_comment_meta( $facepile_comment->comment_ID, 'url', true );
79 | if ( empty( $source ) ) {
80 | $source = get_comment_meta( $facepile_comment->comment_ID, 'webmention_source_url', true );
81 | }
82 |
83 | $kind = $facepile_comment->comment_type;
84 | }
85 |
86 | $classes = array(
87 | 'bookmark' => 'p-bookmark',
88 | 'like' => 'p-like',
89 | 'repost' => 'p-repost',
90 | );
91 | $class = isset( $classes[ $kind ] ) ? esc_attr( $classes[ $kind ] ) : '';
92 |
93 | $titles = array(
94 | 'bookmark' => '… bookmarked this!',
95 | 'like' => '… liked this!',
96 | 'repost' => '… reposted this!',
97 | );
98 | $title_attr = isset( $titles[ $kind ] ) ? esc_attr( $titles[ $kind ] ) : '';
99 |
100 | if ( ! empty( $source ) ) {
101 | $el = '' .
102 | '' . $avatar . '' .
103 | ( ! empty( $attributes['icons'] ) && ! empty( $kind )
104 | ? ''
105 | : ''
106 | ) .
107 | "\n";
108 | } else {
109 | $el = '' .
110 | '' . $avatar . '' .
111 | ( ! empty( $attributes['icons'] ) && ! empty( $kind )
112 | ? ''
113 | : ''
114 | ) .
115 | "\n";
116 | }
117 |
118 | $icon_style = '';
119 | if ( ! empty( $attributes['color'] ) ) {
120 | $icon_style .= "color:{$attributes['color']};";
121 | }
122 | if ( ! empty( $attributes['iconBackgroundColor'] ) ) {
123 | $icon_style .= "background-color:{$attributes['iconBackgroundColor']};";
124 | }
125 |
126 | if ( ! empty( $icon_style ) ) {
127 | $processor = new \WP_HTML_Tag_Processor( $el );
128 | $processor->next_tag( 'svg' );
129 |
130 | $processor->set_attribute( 'style', esc_attr( $icon_style ) );
131 | $el = $processor->get_updated_html();
132 | }
133 |
134 | $output .= $el;
135 | }
136 |
137 | if ( ! empty( $attributes['avatarSize'] ) ) {
138 | $avatar_size = esc_attr( (int) $attributes['avatarSize'] );
139 | $opening_ul_tag = "";
140 | } else {
141 | $opening_ul_tag = '';
142 | }
143 |
144 | $output = $opening_ul_tag . trim( $output ) . '
';
145 | }
146 |
147 | $wrapper_attributes = get_block_wrapper_attributes();
148 | ?>
149 |
150 | >
151 |
152 |
153 |
--------------------------------------------------------------------------------
/blocks/facepile/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-i18n',
10 | ),
11 | );
12 |
--------------------------------------------------------------------------------
/blocks/facepile/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, i18n ) => {
2 | const { registerBlockType } = blocks;
3 | const { createElement: el } = element;
4 | const { BlockControls, InnerBlocks, useBlockProps } = blockEditor;
5 | const { __ } = i18n;
6 |
7 | registerBlockType( 'indieblocks/facepile', {
8 | icon: el(
9 | 'svg',
10 | {
11 | xmlns: 'http://www.w3.org/2000/svg',
12 | viewBox: '0 0 24 24',
13 | },
14 | el( 'path', {
15 | d: 'M12 4a8 8 0 0 0-8 8 8 8 0 0 0 6.64 7.883 8 8 0 0 0 .786.096A8 8 0 0 0 12 20a8 8 0 0 0 8-8 8 8 0 0 0-8-8zm0 1.5a6.5 6.5 0 0 1 6.5 6.5 6.5 6.5 0 0 1-.678 2.875 12.5 9 0 0 0-4.576-.855 3.5 3.5 0 0 0 2.254-3.27 3.5 3.5 0 0 0-3.5-3.5 3.5 3.5 0 0 0-3.5 3.5 3.5 3.5 0 0 0 2.432 3.332 12.5 9 0 0 0-4.59 1.1A6.5 6.5 0 0 1 5.5 12 6.5 6.5 0 0 1 12 5.5z',
16 | } )
17 | ),
18 | edit: ( props ) => {
19 | return el(
20 | 'div',
21 | useBlockProps(),
22 | el( BlockControls ),
23 | el( InnerBlocks, {
24 | template: [
25 | [
26 | 'core/heading',
27 | {
28 | content: __( 'Likes, Bookmarks, and Reposts', 'indieblocks' ),
29 | },
30 | ],
31 | [ 'indieblocks/facepile-content' ],
32 | ],
33 | templateLock: false,
34 | } )
35 | );
36 | },
37 | save: ( props ) => el( 'div', useBlockProps.save(), el( InnerBlocks.Content ) ),
38 | } );
39 | } )( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.i18n );
40 |
--------------------------------------------------------------------------------
/blocks/facepile/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Facepile",
4 | "description": "Contains the blocks to display Webmention “likes,” “reposts,” etc. as a so-called facepile.",
5 | "name": "indieblocks/facepile",
6 | "category": "theme",
7 | "editorScript": "file:./block.js",
8 | "render": "file:./render.php",
9 | "usesContext": [ "postId", "postType" ],
10 | "textdomain": "indieblocks",
11 | "supports": {
12 | "color": {
13 | "gradients": true,
14 | "link": true,
15 | "__experimentalDefaultControls": {
16 | "background": true,
17 | "text": true,
18 | "link": true
19 | }
20 | },
21 | "typography": {
22 | "fontSize": true,
23 | "lineHeight": true,
24 | "__experimentalFontFamily": true,
25 | "__experimentalFontWeight": true,
26 | "__experimentalFontStyle": true,
27 | "__experimentalTextTransform": true,
28 | "__experimentalTextDecoration": true,
29 | "__experimentalLetterSpacing": true,
30 | "__experimentalDefaultControls": {
31 | "fontSize": true
32 | }
33 | },
34 | "spacing": {
35 | "margin": ["top", "bottom"],
36 | "padding": false,
37 | "blockGap": false
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/blocks/facepile/render.php:
--------------------------------------------------------------------------------
1 | context['postId'] ) ) {
7 | $post_id = $block->context['postId']; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
8 | } elseif ( in_the_loop() ) {
9 | $post_id = get_the_ID(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
10 | }
11 |
12 | if ( empty( $post_id ) ) {
13 | return;
14 | }
15 |
16 | $render = false;
17 | $types = array( 'bookmark', 'like', 'repost' );
18 |
19 | $facepile_content_blocks = \IndieBlocks\parse_inner_blocks( $block->parsed_block['innerBlocks'], 'indieblocks/facepile-content' );
20 | foreach ( $facepile_content_blocks as $inner_block ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
21 | if ( isset( $inner_block['attrs']['type'] ) && is_array( $inner_block['attrs']['type'] ) ) {
22 | // If the `$type` attribute is set, use that.
23 | $types = $inner_block['attrs']['type'];
24 | }
25 |
26 | $facepile_comments = \IndieBlocks\get_facepile_comments( $post_id, $types );
27 | if ( ! empty( $facepile_comments ) ) {
28 | // As soon as we've found some "facepile comments," we're good. No need to process any other inner blocks.
29 | // @todo: Stop searching after the first result, too.
30 | $render = true;
31 | break;
32 | }
33 | }
34 |
35 | if ( ! $render ) {
36 | return;
37 | }
38 |
39 | echo $block->render( array( 'dynamic' => false ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
40 |
--------------------------------------------------------------------------------
/blocks/like/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-i18n',
11 | 'indieblocks-common',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/like/block.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-like > .h-cite:first-child > p {
2 | margin-block: 0;
3 | }
4 |
5 | .wp-block-indieblocks-like > .block-editor-inner-blocks {
6 | margin-block: 1em;
7 | min-height: 35px;
8 | }
9 |
--------------------------------------------------------------------------------
/blocks/like/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, data, i18n, IndieBlocks ) => {
2 | const { createBlock, registerBlockType } = blocks;
3 | const { createElement: el, renderToString, useEffect } = element;
4 | const { BlockControls, InnerBlocks, InspectorControls, useBlockProps } = blockEditor;
5 | const { PanelBody, Placeholder, ToggleControl, TextControl } = components;
6 | const { useSelect } = data;
7 | const { __ } = i18n;
8 |
9 | registerBlockType( 'indieblocks/like', {
10 | icon: el(
11 | 'svg',
12 | {
13 | xmlns: 'http://www.w3.org/2000/svg',
14 | viewBox: '0 0 24 24',
15 | },
16 | el( 'path', {
17 | d: 'M7.785 5.49a4.536 4.536 0 0 0-4.535 4.537C3.25 14.564 9 17.25 12 19.75c3-2.5 8.75-5.186 8.75-9.723a4.536 4.536 0 0 0-4.535-4.537c-1.881 0-3.54 1.128-4.215 2.76-.675-1.632-2.334-2.76-4.215-2.76z',
18 | } )
19 | ),
20 | edit: ( props ) => {
21 | const url = props.attributes.url;
22 | const customTitle = props.attributes.customTitle;
23 | const title = props.attributes.title || ''; // May not be present in the saved HTML, so we need a fallback value even when `block.json` contains a default.
24 | const customAuthor = props.attributes.customAuthor;
25 | const author = props.attributes.author || '';
26 |
27 | const updateEmpty = ( empty ) => {
28 | props.setAttributes( { empty } );
29 | };
30 |
31 | const { parentClientId, innerBlocks } = useSelect( ( select ) => {
32 | const parentClientId = select( 'core/block-editor' ).getBlockHierarchyRootClientId( props.clientId );
33 |
34 | return {
35 | parentClientId: parentClientId,
36 | innerBlocks: select( 'core/block-editor' ).getBlocks( parentClientId ),
37 | };
38 | }, [] );
39 |
40 | // To determine whether `.e-content` and `InnerBlocks.Content`
41 | // should be saved (and echoed).
42 | useEffect( () => {
43 | let empty = true;
44 |
45 | if ( innerBlocks.length > 1 ) {
46 | // More than one child block.
47 | empty = false;
48 | }
49 |
50 | if (
51 | 'undefined' !== typeof innerBlocks[ 0 ] &&
52 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.content &&
53 | innerBlocks[ 0 ].attributes.content.length
54 | ) {
55 | // A non-empty paragraph or heading. Empty paragraphs are
56 | // almost unavoidable, so it's important to get this right.
57 | empty = false;
58 | }
59 |
60 | if (
61 | 'undefined' !== typeof innerBlocks[ 0 ] &&
62 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.href &&
63 | innerBlocks[ 0 ].attributes.href.length
64 | ) {
65 | // A non-empty image.
66 | empty = false;
67 | }
68 |
69 | if ( 'undefined' !== typeof innerBlocks[ 0 ] && innerBlocks[ 0 ].innerBlocks.length ) {
70 | // A quote or gallery, empty or not.
71 | empty = false;
72 | }
73 |
74 | updateEmpty( empty );
75 | }, [ innerBlocks, updateEmpty ] );
76 |
77 | const placeholderProps = {
78 | icon: el(
79 | 'svg',
80 | {
81 | xmlns: 'http://www.w3.org/2000/svg',
82 | viewBox: '0 0 24 24',
83 | },
84 | el( 'path', {
85 | d: 'M7.785 5.49a4.536 4.536 0 0 0-4.535 4.537C3.25 14.564 9 17.25 12 19.75c3-2.5 8.75-5.186 8.75-9.723a4.536 4.536 0 0 0-4.535-4.537c-1.881 0-3.54 1.128-4.215 2.76-.675-1.632-2.334-2.76-4.215-2.76z',
86 | } )
87 | ),
88 | label: __( 'Like', 'indieblocks' ),
89 | isColumnLayout: true,
90 | };
91 |
92 | if ( ! url || 'undefined' === url ) {
93 | placeholderProps.instructions = __(
94 | 'Add a URL and have WordPress automatically generate a correctly microformatted introductory paragraph.',
95 | 'indieblocks'
96 | );
97 | }
98 |
99 | const titleProps = {
100 | label: __( 'Title', 'indieblocks' ),
101 | value: title,
102 | onChange: ( value ) => {
103 | props.setAttributes( { title: value } );
104 | },
105 | };
106 |
107 | if ( ! customTitle ) {
108 | titleProps.readOnly = 'readonly';
109 | }
110 |
111 | const authorProps = {
112 | label: __( 'Author', 'indieblocks' ),
113 | value: author,
114 | onChange: ( value ) => {
115 | props.setAttributes( { author: value } );
116 | },
117 | };
118 |
119 | if ( ! customAuthor ) {
120 | authorProps.readOnly = 'readonly';
121 | }
122 |
123 | return el(
124 | 'div',
125 | useBlockProps(),
126 | el( BlockControls ),
127 | props.isSelected || ! url || 'undefined' === url
128 | ? el(
129 | Placeholder,
130 | placeholderProps,
131 | el(
132 | InspectorControls,
133 | { key: 'inspector' },
134 | el(
135 | PanelBody,
136 | {
137 | title: __( 'Title and Author' ),
138 | initialOpen: true,
139 | },
140 | el( TextControl, titleProps ),
141 | el( ToggleControl, {
142 | label: __( 'Customize title', 'indieblocks' ),
143 | checked: customTitle,
144 | onChange: ( value ) => {
145 | props.setAttributes( {
146 | customTitle: value,
147 | } );
148 | },
149 | } ),
150 | el( TextControl, authorProps ),
151 | el( ToggleControl, {
152 | label: __( 'Customize author', 'indieblocks' ),
153 | checked: customAuthor,
154 | onChange: ( value ) => {
155 | props.setAttributes( {
156 | customAuthor: value,
157 | } );
158 | },
159 | } )
160 | )
161 | ),
162 | el( TextControl, {
163 | label: __( 'URL', 'indieblocks' ),
164 | value: url,
165 | onChange: ( value ) => {
166 | props.setAttributes( { url: value } );
167 | },
168 | onKeyDown: ( event ) => {
169 | if ( 13 === event.keyCode ) {
170 | IndieBlocks.updateMeta( props );
171 | }
172 | },
173 | onBlur: () => {
174 | IndieBlocks.updateMeta( props );
175 | },
176 | } )
177 | )
178 | : IndieBlocks.hCite( 'u-like-of', props.attributes ),
179 | el( InnerBlocks, {
180 | template: [ [ 'core/paragraph' ] ],
181 | templateLock: false,
182 | } ) // Always **show** (editable) `InnerBlocks`.
183 | );
184 | },
185 | save: ( props ) =>
186 | el(
187 | 'div',
188 | useBlockProps.save(),
189 | ! props.attributes.url || 'undefined' === props.attributes.url
190 | ? null // Can't do much without a URL.
191 | : IndieBlocks.hCite( 'u-like-of', props.attributes ),
192 | ! props.attributes.empty ? el( 'div', { className: 'e-content' }, el( InnerBlocks.Content ) ) : null
193 | ),
194 | transforms: {
195 | from: [
196 | {
197 | type: 'block',
198 | blocks: [ 'indieblocks/context' ],
199 | transform: ( { url } ) => {
200 | return createBlock( 'indieblocks/like', { url } );
201 | },
202 | },
203 | ],
204 | to: [
205 | {
206 | type: 'block',
207 | blocks: [ 'core/group' ],
208 | transform: ( attributes, innerBlocks ) => {
209 | return createBlock( 'core/group', attributes, [
210 | createBlock( 'core/html', {
211 | content: renderToString( IndieBlocks.hCite( 'u-like-of', attributes ) ),
212 | } ),
213 | createBlock( 'core/group', { className: 'e-content' }, innerBlocks ),
214 | ] );
215 | },
216 | },
217 | {
218 | type: 'block',
219 | blocks: [ 'indieblocks/bookmark' ],
220 | transform: ( attributes, innerBlocks ) => {
221 | return createBlock( 'indieblocks/bookmark', attributes, innerBlocks );
222 | },
223 | },
224 | ],
225 | },
226 | } );
227 | } )(
228 | window.wp.blocks,
229 | window.wp.element,
230 | window.wp.blockEditor,
231 | window.wp.components,
232 | window.wp.data,
233 | window.wp.i18n,
234 | window.IndieBlocks
235 | );
236 |
--------------------------------------------------------------------------------
/blocks/like/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Like",
4 | "description": "Show your appreciation for a certain web page or post.",
5 | "name": "indieblocks/like",
6 | "category": "text",
7 | "editorScript": "file:./block.js",
8 | "style": "file:./block.css",
9 | "attributes": {
10 | "url": {
11 | "type": "string",
12 | "source": "attribute",
13 | "selector": ".u-url",
14 | "attribute": "href",
15 | "default": ""
16 | },
17 | "customTitle": {
18 | "type": "boolean",
19 | "default": false
20 | },
21 | "title": {
22 | "type": "string",
23 | "source": "text",
24 | "selector": ".p-name",
25 | "default": ""
26 | },
27 | "customAuthor": {
28 | "type": "boolean",
29 | "default": false
30 | },
31 | "author": {
32 | "type": "string",
33 | "source": "text",
34 | "selector": ".p-author",
35 | "default": ""
36 | },
37 | "empty": {
38 | "type": "boolean",
39 | "default": true
40 | }
41 | },
42 | "example": {
43 | "attributes": {
44 | "url": "https://example.org/",
45 | "title": "Example Domain",
46 | "author": "Alice"
47 | }
48 | },
49 | "textdomain": "indieblocks"
50 | }
51 |
--------------------------------------------------------------------------------
/blocks/link-preview/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-core-data',
10 | 'wp-i18n',
11 | ),
12 | );
13 |
--------------------------------------------------------------------------------
/blocks/link-preview/block.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-link-preview {
2 | float: none !important;
3 | box-sizing: border-box;
4 | max-width: 550px !important;
5 | overflow: hidden;
6 | }
7 |
8 | .wp-block-indieblocks-link-preview .indieblocks-card {
9 | display: flex;
10 | height: 90px;
11 | line-height: 1.4;
12 | }
13 |
14 | .wp-block-indieblocks-link-preview .indieblocks-card-thumbnail {
15 | background: #eee;
16 | flex-shrink: 0;
17 | width: 90px;
18 | }
19 |
20 | .wp-block-indieblocks-link-preview .indieblocks-card-thumbnail img {
21 | display: block;
22 | object-fit: cover;
23 | }
24 |
25 | .wp-block-indieblocks-link-preview .indieblocks-card-body {
26 | box-sizing: border-box;
27 | padding: 0.67em;
28 | }
29 |
30 | .wp-block-indieblocks-link-preview .indieblocks-card-body strong {
31 | display: block;
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | white-space: nowrap;
35 | }
36 |
37 | .wp-block-indieblocks-link-preview .indieblocks-card-body small {
38 | display: block;
39 | overflow: hidden;
40 | text-overflow: ellipsis;
41 | white-space: nowrap;
42 | }
43 |
--------------------------------------------------------------------------------
/blocks/link-preview/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, coreData, i18n ) => {
2 | const { registerBlockType } = blocks;
3 | const { createElement: el } = element;
4 | const { BlockControls, useBlockProps, __experimentalUseBorderProps } = blockEditor;
5 | const { useEntityRecord } = coreData;
6 | const { __ } = i18n;
7 |
8 | registerBlockType( 'indieblocks/link-preview', {
9 | edit: ( props ) => {
10 | const { record, isResolving } = useEntityRecord( 'postType', props.context.postType, props.context.postId );
11 |
12 | const title = record?.indieblocks_link_preview?.title ?? '';
13 | const cardUrl = record?.indieblocks_link_preview?.url ?? '';
14 | const thumbnail = record?.indieblocks_link_preview?.thumbnail ?? '';
15 |
16 | const borderProps = __experimentalUseBorderProps( props.attributes );
17 | const bodyProps = { className: 'indieblocks-card-body' };
18 | if ( 'undefined' !== typeof borderProps.style && 'undefined' !== typeof borderProps.style.borderWidth ) {
19 | bodyProps.style = {
20 | width: 'calc(100% - 90px - ' + borderProps.style.borderWidth + ')',
21 | };
22 | }
23 |
24 | const blockProps = useBlockProps();
25 |
26 | return el(
27 | 'div',
28 | {
29 | ...blockProps,
30 | style: { ...blockProps.style, ...borderProps.style },
31 | },
32 | el( BlockControls ),
33 | title.length && cardUrl.length
34 | ? el(
35 | 'a',
36 | { className: 'indieblocks-card' },
37 | el(
38 | 'div',
39 | {
40 | className: 'indieblocks-card-thumbnail',
41 | style: {
42 | ...borderProps.style,
43 | borderBlock: 'none',
44 | borderInlineStart: 'none',
45 | borderRadius: '0 !important',
46 | },
47 | },
48 | thumbnail
49 | ? el( 'img', {
50 | src: thumbnail,
51 | width: 90,
52 | height: 90,
53 | alt: '',
54 | } )
55 | : null
56 | ),
57 | el(
58 | 'div',
59 | bodyProps,
60 | el( 'strong', {}, title ),
61 | el( 'small', {}, new URL( cardUrl ).hostname.replace( /^www\./, '' ) )
62 | )
63 | )
64 | : el(
65 | 'div',
66 | { className: 'indieblocks-card' },
67 | el( 'div', {
68 | className: 'indieblocks-card-thumbnail',
69 | style: {
70 | ...borderProps.style,
71 | borderBlock: 'none',
72 | borderInlineStart: 'none',
73 | borderRadius: '0 !important',
74 | },
75 | } ),
76 | el(
77 | 'div',
78 | bodyProps,
79 | el(
80 | 'strong',
81 | { style: { fontWeight: 'normal' } },
82 | props.context.postId
83 | ? __( 'No link preview card', 'indieblocks' )
84 | : __( 'Link Preview', 'indieblocks' )
85 | )
86 | )
87 | )
88 | );
89 | },
90 | } );
91 | } )( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.coreData, window.wp.i18n );
92 |
--------------------------------------------------------------------------------
/blocks/link-preview/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Link Preview",
4 | "description": "Highlight a post’s first hyperlink with a link preview card.",
5 | "name": "indieblocks/link-preview",
6 | "category": "theme",
7 | "icon": "admin-links",
8 | "editorScript": "file:./block.js",
9 | "style": "file:./block.css",
10 | "render": "file:./render.php",
11 | "usesContext": [ "postId", "postType" ],
12 | "textdomain": "indieblocks",
13 | "supports": {
14 | "__experimentalBorder": {
15 | "color": true,
16 | "radius": true,
17 | "width": true,
18 | "__experimentalSkipSerialization": true,
19 | "__experimentalDefaultControls": {
20 | "color": true,
21 | "radius": true,
22 | "width": true
23 | }
24 | },
25 | "color": {
26 | "text": false,
27 | "link": true,
28 | "background": true
29 | },
30 | "typography": {
31 | "fontSize": true,
32 | "lineHeight": true,
33 | "__experimentalFontFamily": true,
34 | "__experimentalFontWeight": true,
35 | "__experimentalFontStyle": true,
36 | "__experimentalTextTransform": true,
37 | "__experimentalTextDecoration": true,
38 | "__experimentalLetterSpacing": true,
39 | "__experimentalDefaultControls": {
40 | "fontSize": true
41 | }
42 | },
43 | "spacing": {
44 | "margin": ["top", "bottom"],
45 | "padding": false,
46 | "blockGap": false
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/blocks/link-preview/render.php:
--------------------------------------------------------------------------------
1 | context['postId'] ) ) {
7 | return;
8 | }
9 |
10 | $card = get_post_meta( $block->context['postId'], '_indieblocks_link_preview', true );
11 |
12 | if ( empty( $card['title'] ) || empty( $card['url'] ) ) {
13 | return;
14 | }
15 |
16 | $border_style = '';
17 |
18 | if ( ! empty( $attributes['borderColor'] ) ) {
19 | $border_style .= "border-color:var(--wp--preset--color--{$attributes['borderColor']});";
20 | } elseif ( ! empty( $attributes['style']['border']['color'] ) ) {
21 | $border_style .= "border-color:{$attributes['style']['border']['color']};";
22 | }
23 |
24 | if ( ! empty( $attributes['style']['border']['width'] ) ) {
25 | $border_style .= "border-width:{$attributes['style']['border']['width']};";
26 | }
27 |
28 | if ( ! empty( $attributes['style']['border']['radius'] ) ) {
29 | $border_style .= "border-radius:{$attributes['style']['border']['radius']};";
30 | }
31 |
32 | $border_style = trim( $border_style );
33 |
34 | $wrapper_attributes = get_block_wrapper_attributes();
35 |
36 | ob_start();
37 | ?>
38 |
57 | next_tag( 'div' );
62 |
63 | $style = $processor->get_attribute( 'style' );
64 | if ( null === $style ) {
65 | $processor->set_attribute( 'style', esc_attr( $border_style ) );
66 | } else {
67 | // Append our styles.
68 | $processor->set_attribute( 'style', esc_attr( rtrim( $style, ';' ) . ";$border_style" ) );
69 | }
70 |
71 | echo $processor->get_updated_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
72 |
--------------------------------------------------------------------------------
/blocks/location/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-core-data',
11 | 'wp-i18n',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/location/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, coreData, i18n ) => {
2 | const { registerBlockType } = blocks;
3 | const { createElement: el, createInterpolateElement: interpolate } = element;
4 | const { BlockControls, InspectorControls, useBlockProps } = blockEditor;
5 | const {
6 | BaseControl,
7 | PanelBody,
8 | ToggleControl,
9 | __experimentalRadio: Radio,
10 | __experimentalRadioGroup: RadioGroup,
11 | } = components;
12 | const { useEntityProp, useEntityRecord } = coreData;
13 | const { __ } = i18n;
14 |
15 | registerBlockType( 'indieblocks/location', {
16 | edit: ( props ) => {
17 | const [ options ] = useEntityProp( 'root', 'site', 'indieblocks_settings' );
18 | const includeWeather = props.attributes.includeWeather;
19 |
20 | const { record, isResolving } = useEntityRecord( 'postType', props.context.postType, props.context.postId );
21 |
22 | const geoAddress = record?.indieblocks_location?.geo_address ?? '';
23 | const description = record?.indieblocks_location?.weather?.description ?? '';
24 |
25 | let temp = includeWeather ? record?.indieblocks_location?.weather?.temperature ?? null : null;
26 | let tempUnit;
27 |
28 | if ( temp ) {
29 | temp = temp > 100 ? temp - 273.15 : temp; // Either degrees Celsius or Kelvin.
30 |
31 | if (
32 | 'undefined' === typeof options ||
33 | ! options.hasOwnProperty( 'weather_units' ) ||
34 | 'metric' === options.weather_units
35 | ) {
36 | tempUnit = ' °C';
37 | } else {
38 | temp = 32 + ( temp * 9 ) / 5;
39 | tempUnit = ' °F';
40 | }
41 |
42 | temp = Math.round( temp );
43 | }
44 |
45 | const sep = props.attributes.separator;
46 |
47 | return el(
48 | 'div',
49 | useBlockProps(),
50 | el( BlockControls ),
51 | el(
52 | InspectorControls,
53 | { key: 'inspector' },
54 | el(
55 | PanelBody,
56 | {
57 | title: __( 'Location', 'indieblocks' ),
58 | initialOpen: true,
59 | },
60 | el( ToggleControl, {
61 | label: __( 'Display weather information', 'indieblocks' ),
62 | checked: includeWeather,
63 | onChange: ( value ) => {
64 | props.setAttributes( {
65 | includeWeather: value,
66 | } );
67 | },
68 | } ),
69 | el(
70 | BaseControl,
71 | {},
72 | el( BaseControl.VisualLabel, { style: { display: 'block' } }, __( 'Separator' ) ),
73 | el(
74 | RadioGroup,
75 | {
76 | label: __( 'Separator', 'indieblocks' ),
77 | checked: sep,
78 | onChange: ( value ) => {
79 | props.setAttributes( {
80 | separator: value,
81 | } );
82 | },
83 | },
84 | el(
85 | Radio,
86 | {
87 | value: ' • ',
88 | style: { paddingInline: '1.25em' },
89 | },
90 | '•'
91 | ),
92 | el(
93 | Radio,
94 | {
95 | value: ' | ',
96 | style: { paddingInline: '1.25em' },
97 | },
98 | '|'
99 | ),
100 | el(
101 | Radio,
102 | {
103 | value: ', ',
104 | style: { paddingInline: '1.25em' },
105 | },
106 | ','
107 | ),
108 | el(
109 | Radio,
110 | {
111 | value: '; ',
112 | style: { paddingInline: '1.25em' },
113 | },
114 | ';'
115 | )
116 | )
117 | )
118 | )
119 | ),
120 | '' !== geoAddress
121 | ? el(
122 | 'span',
123 | { className: 'h-geo' },
124 | temp
125 | ? interpolate(
126 | '' +
127 | geoAddress +
128 | '' +
129 | sep +
130 | '' +
131 | temp +
132 | tempUnit +
133 | ', ' +
134 | description.toLowerCase() +
135 | '',
136 | {
137 | a: el( 'span', {
138 | className: 'p-name',
139 | } ),
140 | b: el( 'span', {
141 | className: 'sep',
142 | 'aria-hidden': 'true',
143 | } ),
144 | c: el( 'span', {} ),
145 | }
146 | )
147 | : el(
148 | 'span',
149 | {
150 | className: 'p-name',
151 | },
152 | geoAddress
153 | )
154 | )
155 | : props.context.postId
156 | ? __( 'No location', 'indieblocks' )
157 | : __( 'Location', 'indieblocks' )
158 | );
159 | },
160 | } );
161 | } )(
162 | window.wp.blocks,
163 | window.wp.element,
164 | window.wp.blockEditor,
165 | window.wp.components,
166 | window.wp.coreData,
167 | window.wp.i18n
168 | );
169 |
--------------------------------------------------------------------------------
/blocks/location/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Location",
4 | "description": "Display a post’s location and weather metadata.",
5 | "name": "indieblocks/location",
6 | "category": "theme",
7 | "icon": "location",
8 | "editorScript": "file:./block.js",
9 | "render": "file:./render.php",
10 | "attributes": {
11 | "includeWeather": {
12 | "type": "boolean",
13 | "default": false
14 | },
15 | "separator": {
16 | "type": "string",
17 | "default": " • "
18 | }
19 | },
20 | "usesContext": [ "postId", "postType" ],
21 | "textdomain": "indieblocks",
22 | "supports": {
23 | "color": {
24 | "gradients": true,
25 | "link": true,
26 | "__experimentalDefaultControls": {
27 | "background": true,
28 | "text": true,
29 | "link": true
30 | }
31 | },
32 | "typography": {
33 | "fontSize": true,
34 | "lineHeight": true,
35 | "__experimentalFontFamily": true,
36 | "__experimentalFontWeight": true,
37 | "__experimentalFontStyle": true,
38 | "__experimentalTextTransform": true,
39 | "__experimentalTextDecoration": true,
40 | "__experimentalLetterSpacing": true,
41 | "__experimentalDefaultControls": {
42 | "fontSize": true
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/blocks/location/render.php:
--------------------------------------------------------------------------------
1 | context['postId'] ) ) {
7 | return;
8 | }
9 |
10 | $location = get_post_meta( $block->context['postId'], 'geo_address', true );
11 |
12 | if ( empty( $location ) ) {
13 | return;
14 | }
15 |
16 | $output = '' . esc_html( $location ) . '';
17 |
18 | if ( ! empty( $attributes['includeWeather'] ) ) {
19 | $weather = get_post_meta( $block->context['postId'], '_indieblocks_weather', true );
20 | }
21 |
22 | if ( ! empty( $weather['description'] ) && ! empty( $weather['temperature'] ) ) {
23 | $temp = $weather['temperature'];
24 | $temp = $temp > 100 // Older plugin versions supported only degrees Celsius, newer versions only Kelvin.
25 | ? $temp - 273.15
26 | : $temp;
27 |
28 | $options = \IndieBlocks\get_options();
29 |
30 | if ( empty( $options['weather_units'] ) || 'metric' === $options['weather_units'] ) {
31 | $temp_unit = ' °C';
32 | } else {
33 | $temp = 32 + $temp * 9 / 5;
34 | $temp_unit = ' °F';
35 | }
36 | $temp = number_format( round( $temp ) ); // Round.
37 |
38 | $sep = ! empty( $attributes['separator'] ) ? $attributes['separator'] : ' • ';
39 | $sep = apply_filters( 'indieblocks_location_separator', $sep, $block->context['postId'] );
40 |
41 | $output .= '' . esc_html( $sep ) . '' . esc_html( $temp . $temp_unit ) . ', ' . esc_html( strtolower( $weather['description'] ) ) . '';
42 | }
43 |
44 | $wrapper_attributes = get_block_wrapper_attributes();
45 | ?>
46 |
47 | >
48 | ' . $output . '', $block->context['postId'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
49 |
50 |
--------------------------------------------------------------------------------
/blocks/reply/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-i18n',
11 | 'indieblocks-common',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/reply/block.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-reply > .h-cite:first-child > p {
2 | margin-block: 0;
3 | }
4 |
5 | .wp-block-indieblocks-reply > .block-editor-inner-blocks {
6 | margin-block: 1em;
7 | min-height: 35px;
8 | }
9 |
--------------------------------------------------------------------------------
/blocks/reply/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, data, i18n, IndieBlocks ) => {
2 | const { createBlock, registerBlockType } = blocks;
3 | const { createElement: el, renderToString, useEffect } = element;
4 | const { BlockControls, InnerBlocks, InspectorControls, useBlockProps } = blockEditor;
5 | const { PanelBody, Placeholder, ToggleControl, TextControl } = components;
6 | const { useSelect } = data;
7 | const { __ } = i18n;
8 |
9 | registerBlockType( 'indieblocks/reply', {
10 | edit: ( props ) => {
11 | const url = props.attributes.url;
12 | const customTitle = props.attributes.customTitle;
13 | const title = props.attributes.title || ''; // May not be present in the saved HTML, so we need a fallback value even when `block.json` contains a default.
14 | const customAuthor = props.attributes.customAuthor;
15 | const author = props.attributes.author || '';
16 |
17 | const updateEmpty = ( empty ) => {
18 | props.setAttributes( { empty } );
19 | };
20 |
21 | const { parentClientId, innerBlocks } = useSelect( ( select ) => {
22 | const parentClientId = select( 'core/block-editor' ).getBlockHierarchyRootClientId( props.clientId );
23 |
24 | return {
25 | parentClientId: parentClientId,
26 | innerBlocks: select( 'core/block-editor' ).getBlocks( parentClientId ),
27 | };
28 | }, [] );
29 |
30 | // To determine whether `.e-content` and `InnerBlocks.Content`
31 | // should be saved (and echoed).
32 | useEffect( () => {
33 | let empty = true;
34 |
35 | if ( innerBlocks.length > 1 ) {
36 | // More than one child block.
37 | empty = false;
38 | }
39 |
40 | if (
41 | 'undefined' !== typeof innerBlocks[ 0 ] &&
42 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.content &&
43 | innerBlocks[ 0 ].attributes.content.length
44 | ) {
45 | // A non-empty paragraph or heading. Empty paragraphs are
46 | // almost unavoidable, so it's important to get this right.
47 | empty = false;
48 | }
49 |
50 | if (
51 | 'undefined' !== typeof innerBlocks[ 0 ] &&
52 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.href &&
53 | innerBlocks[ 0 ].attributes.href.length
54 | ) {
55 | // A non-empty image.
56 | empty = false;
57 | }
58 |
59 | if ( 'undefined' !== typeof innerBlocks[ 0 ] && innerBlocks[ 0 ].innerBlocks.length ) {
60 | // A quote or gallery, empty or not.
61 | empty = false;
62 | }
63 |
64 | updateEmpty( empty );
65 | }, [ innerBlocks, updateEmpty ] );
66 |
67 | const placeholderProps = {
68 | icon: 'admin-comments',
69 | label: __( 'Reply', 'indieblocks' ),
70 | isColumnLayout: true,
71 | };
72 |
73 | if ( ! url || 'undefined' === url ) {
74 | placeholderProps.instructions = __(
75 | 'Add a URL and have WordPress automatically generate a correctly microformatted introductory paragraph.',
76 | 'indieblocks'
77 | );
78 | }
79 |
80 | const titleProps = {
81 | label: __( 'Title', 'indieblocks' ),
82 | value: title,
83 | onChange: ( value ) => {
84 | props.setAttributes( { title: value } );
85 | },
86 | };
87 |
88 | if ( ! customTitle ) {
89 | titleProps.readOnly = 'readonly';
90 | }
91 |
92 | const authorProps = {
93 | label: __( 'Author', 'indieblocks' ),
94 | value: author,
95 | onChange: ( value ) => {
96 | props.setAttributes( { author: value } );
97 | },
98 | };
99 |
100 | if ( ! customAuthor ) {
101 | authorProps.readOnly = 'readonly';
102 | }
103 |
104 | return el(
105 | 'div',
106 | useBlockProps(),
107 | el( BlockControls ),
108 | props.isSelected || ! url || 'undefined' === url
109 | ? el(
110 | Placeholder,
111 | placeholderProps,
112 | el(
113 | InspectorControls,
114 | { key: 'inspector' },
115 | el(
116 | PanelBody,
117 | {
118 | title: __( 'Title and Author' ),
119 | initialOpen: true,
120 | },
121 | el( TextControl, titleProps ),
122 | el( ToggleControl, {
123 | label: __( 'Customize title', 'indieblocks' ),
124 | checked: customTitle,
125 | onChange: ( value ) => {
126 | props.setAttributes( {
127 | customTitle: value,
128 | } );
129 | },
130 | } ),
131 | el( TextControl, authorProps ),
132 | el( ToggleControl, {
133 | label: __( 'Customize author', 'indieblocks' ),
134 | checked: customAuthor,
135 | onChange: ( value ) => {
136 | props.setAttributes( {
137 | customAuthor: value,
138 | } );
139 | },
140 | } )
141 | )
142 | ),
143 | el( TextControl, {
144 | label: __( 'URL', 'indieblocks' ),
145 | value: url,
146 | onChange: ( value ) => {
147 | props.setAttributes( { url: value } );
148 | },
149 | onKeyDown: ( event ) => {
150 | if ( 13 === event.keyCode ) {
151 | IndieBlocks.updateMeta( props );
152 | }
153 | },
154 | onBlur: () => {
155 | IndieBlocks.updateMeta( props );
156 | },
157 | } )
158 | )
159 | : IndieBlocks.hCite( 'u-in-reply-to', props.attributes ),
160 | el( InnerBlocks, {
161 | template: [ [ 'core/paragraph' ] ],
162 | templateLock: false,
163 | } ) // Always **show** (editable) `InnerBlocks`.
164 | );
165 | },
166 | save: ( props ) =>
167 | el(
168 | 'div',
169 | useBlockProps.save(),
170 | ! props.attributes.url || 'undefined' === props.attributes.url
171 | ? null // Can't do much without a URL.
172 | : IndieBlocks.hCite( 'u-in-reply-to', props.attributes ),
173 | ! props.attributes.empty ? el( 'div', { className: 'e-content' }, el( InnerBlocks.Content ) ) : null
174 | ),
175 | transforms: {
176 | from: [
177 | {
178 | type: 'block',
179 | blocks: [ 'indieblocks/context' ],
180 | transform: ( { url } ) => {
181 | return createBlock( 'indieblocks/reply', { url } );
182 | },
183 | },
184 | ],
185 | to: [
186 | {
187 | type: 'block',
188 | blocks: [ 'core/group' ],
189 | transform: ( attributes, innerBlocks ) => {
190 | return createBlock( 'core/group', attributes, [
191 | createBlock( 'core/html', {
192 | content: renderToString( IndieBlocks.hCite( 'u-in-reply-to', attributes ) ),
193 | } ),
194 | createBlock( 'core/group', { className: 'e-content' }, innerBlocks ),
195 | ] );
196 | },
197 | },
198 | ],
199 | },
200 | } );
201 | } )(
202 | window.wp.blocks,
203 | window.wp.element,
204 | window.wp.blockEditor,
205 | window.wp.components,
206 | window.wp.data,
207 | window.wp.i18n,
208 | window.IndieBlocks
209 | );
210 |
--------------------------------------------------------------------------------
/blocks/reply/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Reply",
4 | "description": "Reply to others’ (or your own) posts and pages.",
5 | "name": "indieblocks/reply",
6 | "category": "text",
7 | "icon": "admin-comments",
8 | "editorScript": "file:./block.js",
9 | "style": "file:./block.css",
10 | "attributes": {
11 | "url": {
12 | "type": "string",
13 | "source": "attribute",
14 | "selector": ".u-url",
15 | "attribute": "href",
16 | "default": ""
17 | },
18 | "customTitle": {
19 | "type": "boolean",
20 | "default": false
21 | },
22 | "title": {
23 | "type": "string",
24 | "source": "text",
25 | "selector": ".u-url",
26 | "default": ""
27 | },
28 | "customAuthor": {
29 | "type": "boolean",
30 | "default": false
31 | },
32 | "author": {
33 | "type": "string",
34 | "source": "text",
35 | "selector": ".p-author",
36 | "default": ""
37 | },
38 | "empty": {
39 | "type": "boolean",
40 | "default": true
41 | }
42 | },
43 | "example": {
44 | "attributes": {
45 | "url": "https://example.org/",
46 | "title": "Example Domain",
47 | "author": "Alice"
48 | }
49 | },
50 | "textdomain": "indieblocks"
51 | }
52 |
--------------------------------------------------------------------------------
/blocks/repost/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-i18n',
11 | 'indieblocks-common',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/repost/block.css:
--------------------------------------------------------------------------------
1 | .wp-block-indieblocks-repost > .h-cite:first-child > p {
2 | margin-block: 0;
3 | }
4 |
5 | .wp-block-indieblocks-repost > blockquote {
6 | margin-block: 1em;
7 | min-height: 35px;
8 | }
9 |
10 | .wp-block-indieblocks-repost > blockquote.is-layout-flow > .block-editor-inner-blocks > .block-editor-block-list__layout > :first-child {
11 | margin-block-start: 0;
12 | }
13 |
14 | .wp-block-indieblocks-repost > blockquote.is-layout-flow > .block-editor-inner-blocks > .block-editor-block-list__layout > :last-child {
15 | margin-block-end: 0;
16 | }
17 |
--------------------------------------------------------------------------------
/blocks/repost/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, data, i18n, IndieBlocks ) => {
2 | const { createBlock, registerBlockType } = blocks;
3 | const { createElement: el, renderToString, useEffect } = element;
4 | const { BlockControls, InnerBlocks, InspectorControls, useBlockProps } = blockEditor;
5 | const { PanelBody, Placeholder, ToggleControl, TextControl } = components;
6 | const { useSelect } = data;
7 | const { __ } = i18n;
8 |
9 | registerBlockType( 'indieblocks/repost', {
10 | icon: el(
11 | 'svg',
12 | {
13 | xmlns: 'http://www.w3.org/2000/svg',
14 | viewBox: '0 0 24 24',
15 | },
16 | el( 'path', {
17 | d: 'M7.25 6a2 2 0 0 0-2 2v6.1l-3-.1 4 4 4-4-3 .1V8h6.25l2-2zM16.75 9.9l-3 .1 4-4 4 4-3-.1V16a2 2 0 0 1-2 2H8.5l2-2h6.25z',
18 | } )
19 | ),
20 | edit: ( props ) => {
21 | const url = props.attributes.url;
22 | const customTitle = props.attributes.customTitle;
23 | const title = props.attributes.title || ''; // May not be present in the saved HTML, so we need a fallback value even when `block.json` contains a default.
24 | const customAuthor = props.attributes.customAuthor;
25 | const author = props.attributes.author || '';
26 |
27 | const updateEmpty = ( empty ) => {
28 | props.setAttributes( { empty } );
29 | };
30 |
31 | const { parentClientId, innerBlocks } = useSelect( ( select ) => {
32 | const parentClientId = select( 'core/block-editor' ).getBlockHierarchyRootClientId( props.clientId );
33 |
34 | return {
35 | parentClientId: parentClientId,
36 | innerBlocks: select( 'core/block-editor' ).getBlocks( parentClientId ),
37 | };
38 | }, [] );
39 |
40 | // To determine whether `.e-content` and `InnerBlocks.Content`
41 | // should be saved (and echoed).
42 | useEffect( () => {
43 | let empty = true;
44 |
45 | if ( innerBlocks.length > 1 ) {
46 | // More than one child block.
47 | empty = false;
48 | }
49 |
50 | if (
51 | 'undefined' !== typeof innerBlocks[ 0 ] &&
52 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.content &&
53 | innerBlocks[ 0 ].attributes.content.length
54 | ) {
55 | // A non-empty paragraph or heading. Empty paragraphs are
56 | // almost unavoidable, so it's important to get this right.
57 | empty = false;
58 | }
59 |
60 | if (
61 | 'undefined' !== typeof innerBlocks[ 0 ] &&
62 | 'undefined' !== typeof innerBlocks[ 0 ].attributes.href &&
63 | innerBlocks[ 0 ].attributes.href.length
64 | ) {
65 | // A non-empty image.
66 | empty = false;
67 | }
68 |
69 | if ( 'undefined' !== typeof innerBlocks[ 0 ] && innerBlocks[ 0 ].innerBlocks.length ) {
70 | // A quote or gallery, empty or not.
71 | empty = false;
72 | }
73 |
74 | updateEmpty( empty );
75 | }, [ innerBlocks, updateEmpty ] );
76 |
77 | const placeholderProps = {
78 | icon: el(
79 | 'svg',
80 | {
81 | xmlns: 'http://www.w3.org/2000/svg',
82 | viewBox: '0 0 24 24',
83 | },
84 | el( 'path', {
85 | d: 'M7.25 6a2 2 0 0 0-2 2v6.1l-3-.1 4 4 4-4-3 .1V8h6.25l2-2zM16.75 9.9l-3 .1 4-4 4 4-3-.1V16a2 2 0 0 1-2 2H8.5l2-2h6.25z',
86 | } )
87 | ),
88 | label: __( 'Repost', 'indieblocks' ),
89 | isColumnLayout: true,
90 | };
91 |
92 | if ( ! url || 'undefined' === url ) {
93 | placeholderProps.instructions = __(
94 | 'Add a URL and have WordPress automatically generate a correctly microformatted introductory paragraph.',
95 | 'indieblocks'
96 | );
97 | }
98 |
99 | const titleProps = {
100 | label: __( 'Title', 'indieblocks' ),
101 | value: title,
102 | onChange: ( value ) => {
103 | props.setAttributes( { title: value } );
104 | },
105 | };
106 |
107 | if ( ! customTitle ) {
108 | titleProps.readOnly = 'readonly';
109 | }
110 |
111 | const authorProps = {
112 | label: __( 'Author', 'indieblocks' ),
113 | value: author,
114 | onChange: ( value ) => {
115 | props.setAttributes( { author: value } );
116 | },
117 | };
118 |
119 | if ( ! customAuthor ) {
120 | authorProps.readOnly = 'readonly';
121 | }
122 |
123 | return el(
124 | 'div',
125 | useBlockProps(),
126 | el( BlockControls ),
127 | props.isSelected || ! url || 'undefined' === url
128 | ? el(
129 | Placeholder,
130 | placeholderProps,
131 | el(
132 | InspectorControls,
133 | { key: 'inspector' },
134 | el(
135 | PanelBody,
136 | {
137 | title: __( 'Title and Author' ),
138 | initialOpen: true,
139 | },
140 | el( TextControl, titleProps ),
141 | el( ToggleControl, {
142 | label: __( 'Customize title', 'indieblocks' ),
143 | checked: customTitle,
144 | onChange: ( value ) => {
145 | props.setAttributes( {
146 | customTitle: value,
147 | } );
148 | },
149 | } ),
150 | el( TextControl, authorProps ),
151 | el( ToggleControl, {
152 | label: __( 'Customize author', 'indieblocks' ),
153 | checked: customAuthor,
154 | onChange: ( value ) => {
155 | props.setAttributes( {
156 | customAuthor: value,
157 | } );
158 | },
159 | } )
160 | )
161 | ),
162 | el( TextControl, {
163 | label: __( 'URL', 'indieblocks' ),
164 | value: url,
165 | onChange: ( value ) => {
166 | props.setAttributes( { url: value } );
167 | },
168 | onKeyDown: ( event ) => {
169 | if ( 13 === event.keyCode ) {
170 | IndieBlocks.updateMeta( props );
171 | }
172 | },
173 | onBlur: () => {
174 | IndieBlocks.updateMeta( props );
175 | },
176 | } )
177 | )
178 | : IndieBlocks.hCite( 'u-repost-of', props.attributes ),
179 | el(
180 | 'blockquote',
181 | { className: 'wp-block-quote is-layout-flow wp-block-quote-is-layout-flow e-content' },
182 | el( InnerBlocks, {
183 | template: [ [ 'core/paragraph' ] ],
184 | templateLock: false,
185 | } ) // Always **show** (editable) `InnerBlocks`.
186 | )
187 | );
188 | },
189 | save: ( props ) =>
190 | el(
191 | 'div',
192 | useBlockProps.save(),
193 | ! props.attributes.url || 'undefined' === props.attributes.url
194 | ? null // Can't do much without a URL.
195 | : IndieBlocks.hCite( 'u-repost-of', props.attributes, InnerBlocks.Content )
196 | ),
197 | transforms: {
198 | from: [
199 | {
200 | type: 'block',
201 | blocks: [ 'indieblocks/context' ],
202 | transform: ( { url } ) => {
203 | return createBlock( 'indieblocks/repost', { url } );
204 | },
205 | },
206 | ],
207 | to: [
208 | {
209 | type: 'block',
210 | blocks: [ 'core/group' ],
211 | transform: ( attributes, innerBlocks ) => {
212 | return createBlock( 'core/group', attributes, [
213 | createBlock( 'core/html', {
214 | content: renderToString( IndieBlocks.hCite( 'u-repost-of', attributes ) ),
215 | } ),
216 | createBlock( 'core/quote', { className: 'e-content' }, innerBlocks ),
217 | ] );
218 | },
219 | },
220 | ],
221 | },
222 | } );
223 | } )(
224 | window.wp.blocks,
225 | window.wp.element,
226 | window.wp.blockEditor,
227 | window.wp.components,
228 | window.wp.data,
229 | window.wp.i18n,
230 | window.IndieBlocks
231 | );
232 |
--------------------------------------------------------------------------------
/blocks/repost/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Repost",
4 | "description": "Use the Repost block to “reblog” another (short) post verbatim while still giving credit.",
5 | "name": "indieblocks/repost",
6 | "category": "text",
7 | "editorScript": "file:./block.js",
8 | "style": "file:./block.css",
9 | "attributes": {
10 | "url": {
11 | "type": "string",
12 | "source": "attribute",
13 | "selector": ".u-url",
14 | "attribute": "href",
15 | "default": ""
16 | },
17 | "customTitle": {
18 | "type": "boolean",
19 | "default": false
20 | },
21 | "title": {
22 | "type": "string",
23 | "source": "text",
24 | "selector": ".p-name",
25 | "default": ""
26 | },
27 | "customAuthor": {
28 | "type": "boolean",
29 | "default": false
30 | },
31 | "author": {
32 | "type": "string",
33 | "source": "text",
34 | "selector": ".p-author",
35 | "default": ""
36 | },
37 | "empty": {
38 | "type": "boolean",
39 | "default": true
40 | }
41 | },
42 | "example": {
43 | "attributes": {
44 | "url": "https://example.org/",
45 | "title": "Example Domain",
46 | "author": "Alice"
47 | }
48 | },
49 | "textdomain": "indieblocks"
50 | }
51 |
--------------------------------------------------------------------------------
/blocks/syndication/block.asset.php:
--------------------------------------------------------------------------------
1 | \IndieBlocks\Plugin::PLUGIN_VERSION,
5 | 'dependencies' => array(
6 | 'wp-blocks',
7 | 'wp-element',
8 | 'wp-block-editor',
9 | 'wp-components',
10 | 'wp-core-data',
11 | 'wp-i18n',
12 | ),
13 | );
14 |
--------------------------------------------------------------------------------
/blocks/syndication/block.js:
--------------------------------------------------------------------------------
1 | ( ( blocks, element, blockEditor, components, coreData, i18n ) => {
2 | const { registerBlockType } = blocks;
3 | const { createElement: el, RawHTML, useState } = element;
4 | const { BlockControls, InspectorControls, useBlockProps } = blockEditor;
5 | const { PanelBody, TextControl } = components;
6 | const { useEntityRecord } = coreData;
7 | const { __ } = i18n;
8 |
9 | const render = ( urls ) => {
10 | let output = '';
11 |
12 | urls.forEach( ( url ) => {
13 | output +=
14 | '' +
17 | url.name +
18 | ', ';
19 | } );
20 |
21 | return output.replace( /[,\s]+$/, '' );
22 | };
23 |
24 | registerBlockType( 'indieblocks/syndication', {
25 | edit: ( props ) => {
26 | const prefix = props.attributes?.prefix ?? '';
27 | const suffix = props.attributes?.suffix ?? '';
28 |
29 | // We'd use `serverSideRender` but it doesn't support passing block
30 | // context to PHP. I.e., rendering in JS better reflects what the
31 | // block will look like on the front end.
32 | // @see https://github.com/WordPress/gutenberg/issues/40714
33 | const { record, isResolving } = useEntityRecord( 'postType', props.context.postType, props.context.postId );
34 | const [ mastodonUrl ] = useState( record?.share_on_mastodon?.url ?? '' );
35 | const [ pixelfedUrl ] = useState( record?.share_on_pixelfed?.url ?? '' );
36 |
37 | const urls = [];
38 |
39 | if ( mastodonUrl ) {
40 | urls.push( {
41 | name: __( 'Mastodon', 'indieblocks' ),
42 | value: mastodonUrl,
43 | } );
44 | }
45 |
46 | if ( pixelfedUrl ) {
47 | urls.push( {
48 | name: __( 'Pixelfed', 'indieblocks' ),
49 | value: pixelfedUrl,
50 | } );
51 | }
52 |
53 | return el(
54 | 'div',
55 | useBlockProps(),
56 | el( BlockControls ),
57 | props.isSelected
58 | ? el(
59 | InspectorControls,
60 | { key: 'inspector' },
61 | el(
62 | PanelBody,
63 | {
64 | title: __( 'Prefix and Suffix', 'indieblocks' ),
65 | initialOpen: true,
66 | },
67 | // @todo: Base these on "proper" `RichText` instances or something.
68 | el( TextControl, {
69 | label: __( 'Prefix', 'indieblocks' ),
70 | value: prefix,
71 | onChange: ( prefix ) => {
72 | props.setAttributes( { prefix } );
73 | },
74 | } ),
75 | el( TextControl, {
76 | label: __( 'Suffix', 'indieblocks' ),
77 | value: suffix,
78 | onChange: ( suffix ) => {
79 | props.setAttributes( { suffix } );
80 | },
81 | } )
82 | )
83 | )
84 | : null,
85 | ! props.context.postId
86 | ? __( 'Syndication Links', 'indieblocks' )
87 | : urls.length
88 | ? RawHTML( { children: prefix + render( urls ) + suffix } )
89 | : prefix + __( 'Syndication Links', 'indieblocks' ) + suffix
90 | );
91 | },
92 | } );
93 | } )(
94 | window.wp.blocks,
95 | window.wp.element,
96 | window.wp.blockEditor,
97 | window.wp.components,
98 | window.wp.coreData,
99 | window.wp.i18n
100 | );
101 |
--------------------------------------------------------------------------------
/blocks/syndication/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": 2,
3 | "title": "Syndication",
4 | "description": "Display syndication links.",
5 | "name": "indieblocks/syndication",
6 | "category": "theme",
7 | "icon": "admin-links",
8 | "editorScript": "file:./block.js",
9 | "render": "file:./render.php",
10 | "usesContext": [ "postId", "postType" ],
11 | "attributes": {
12 | "prefix": {
13 | "type": "string",
14 | "default": ""
15 | },
16 | "suffix": {
17 | "type": "string",
18 | "default": ""
19 | }
20 | },
21 | "textdomain": "indieblocks",
22 | "supports": {
23 | "color": {
24 | "gradients": true,
25 | "link": true,
26 | "__experimentalDefaultControls": {
27 | "background": true,
28 | "text": true,
29 | "link": true
30 | }
31 | },
32 | "typography": {
33 | "fontSize": true,
34 | "lineHeight": true,
35 | "__experimentalFontFamily": true,
36 | "__experimentalFontWeight": true,
37 | "__experimentalFontStyle": true,
38 | "__experimentalTextTransform": true,
39 | "__experimentalTextDecoration": true,
40 | "__experimentalLetterSpacing": true,
41 | "__experimentalDefaultControls": {
42 | "fontSize": true
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/blocks/syndication/render.php:
--------------------------------------------------------------------------------
1 | context['postId'] ) ) {
9 | return;
10 | }
11 |
12 | $syndication_urls = array_filter(
13 | array(
14 | __( 'Mastodon', 'indieblocks' ) => get_post_meta( $block->context['postId'], '_share_on_mastodon_url', true ),
15 | __( 'Pixelfed', 'indieblocks' ) => get_post_meta( $block->context['postId'], '_share_on_pixelfed_url', true ),
16 | )
17 | );
18 |
19 | // Allow developers to parse in other plugins' links.
20 | $syndication_urls = apply_filters( 'indieblocks_syndication_links', $syndication_urls, $block->context['postId'] );
21 |
22 | if ( empty( $syndication_urls ) ) {
23 | return;
24 | }
25 |
26 | $output = esc_html( isset( $attributes['prefix'] ) ? trim( $attributes['prefix'] ) . ' ' : '' );
27 |
28 | foreach ( $syndication_urls as $name => $syndication_url ) {
29 | $output .= '' . esc_html( $name ) . ', ';
30 | }
31 |
32 | $output = rtrim( $output, ', ' ) . esc_html( isset( $attributes['suffix'] ) ? ' ' . trim( $attributes['suffix'] ) : '' );
33 |
34 | $wrapper_attributes = get_block_wrapper_attributes();
35 | ?>
36 |
37 | >
38 |
39 |
40 |
--------------------------------------------------------------------------------
/build/vendor/autoload.php:
--------------------------------------------------------------------------------
1 | $vendorDir . '/composer/InstalledVersions.php',
10 | 'IndieBlocks\\Blocks' => $baseDir . '/../includes/class-blocks.php',
11 | 'IndieBlocks\\Commands\\Commands' => $baseDir . '/../includes/commands/class-commands.php',
12 | 'IndieBlocks\\Feeds' => $baseDir . '/../includes/class-feeds.php',
13 | 'IndieBlocks\\Location' => $baseDir . '/../includes/class-location.php',
14 | 'IndieBlocks\\Masterminds\\HTML5' => $vendorDir . '/masterminds/html5/src/HTML5.php',
15 | 'IndieBlocks\\Masterminds\\HTML5\\Elements' => $vendorDir . '/masterminds/html5/src/HTML5/Elements.php',
16 | 'IndieBlocks\\Masterminds\\HTML5\\Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Entities.php',
17 | 'IndieBlocks\\Masterminds\\HTML5\\Exception' => $vendorDir . '/masterminds/html5/src/HTML5/Exception.php',
18 | 'IndieBlocks\\Masterminds\\HTML5\\InstructionProcessor' => $vendorDir . '/masterminds/html5/src/HTML5/InstructionProcessor.php',
19 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\CharacterReference' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php',
20 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php',
21 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\EventHandler' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/EventHandler.php',
22 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\FileInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php',
23 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\InputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/InputStream.php',
24 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\ParseError' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/ParseError.php',
25 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\Scanner' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Scanner.php',
26 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\StringInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php',
27 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\Tokenizer' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php',
28 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\TreeBuildingRules' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php',
29 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\UTF8Utils' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php',
30 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\HTML5Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php',
31 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\OutputRules' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php',
32 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\RulesInterface' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php',
33 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\Traverser' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/Traverser.php',
34 | 'IndieBlocks\\Michelf\\Markdown' => $vendorDir . '/michelf/php-markdown/Michelf/Markdown.php',
35 | 'IndieBlocks\\Michelf\\MarkdownExtra' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownExtra.php',
36 | 'IndieBlocks\\Michelf\\MarkdownInterface' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownInterface.php',
37 | 'IndieBlocks\\Micropub_Compat' => $baseDir . '/../includes/class-micropub-compat.php',
38 | 'IndieBlocks\\Mimey\\MimeMappingGenerator' => $vendorDir . '/ralouphie/mimey/src/MimeMappingGenerator.php',
39 | 'IndieBlocks\\Mimey\\MimeTypes' => $vendorDir . '/ralouphie/mimey/src/MimeTypes.php',
40 | 'IndieBlocks\\Mimey\\MimeTypesInterface' => $vendorDir . '/ralouphie/mimey/src/MimeTypesInterface.php',
41 | 'IndieBlocks\\Options_Handler' => $baseDir . '/../includes/class-options-handler.php',
42 | 'IndieBlocks\\Parser' => $baseDir . '/../includes/class-parser.php',
43 | 'IndieBlocks\\Plugin' => $baseDir . '/../includes/class-plugin.php',
44 | 'IndieBlocks\\Post_Types' => $baseDir . '/../includes/class-post-types.php',
45 | 'IndieBlocks\\Preview_Cards' => $baseDir . '/../includes/class-preview-cards.php',
46 | 'IndieBlocks\\Theme_Mf2' => $baseDir . '/../includes/class-theme-mf2.php',
47 | 'IndieBlocks\\Webmention\\Webmention' => $baseDir . '/../includes/webmention/class-webmention.php',
48 | 'IndieBlocks\\Webmention\\Webmention_Parser' => $baseDir . '/../includes/webmention/class-webmention-parser.php',
49 | 'IndieBlocks\\Webmention\\Webmention_Receiver' => $baseDir . '/../includes/webmention/class-webmention-receiver.php',
50 | 'IndieBlocks\\Webmention\\Webmention_Sender' => $baseDir . '/../includes/webmention/class-webmention-sender.php',
51 | );
52 |
--------------------------------------------------------------------------------
/build/vendor/composer/autoload_files.php:
--------------------------------------------------------------------------------
1 | $vendorDir . '/mf2/mf2/Mf2/Parser.php',
10 | 'IndieBlocks_a01125dfebcda7ec3333dcd2d57ad8f2' => $baseDir . '/../includes/functions.php',
11 | );
12 |
--------------------------------------------------------------------------------
/build/vendor/composer/autoload_namespaces.php:
--------------------------------------------------------------------------------
1 | array($vendorDir . '/ralouphie/mimey/src'),
10 | 'IndieBlocks\\Michelf\\' => array($vendorDir . '/michelf/php-markdown/Michelf'),
11 | 'IndieBlocks\\Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
12 | );
13 |
--------------------------------------------------------------------------------
/build/vendor/composer/autoload_real.php:
--------------------------------------------------------------------------------
1 | setClassMapAuthoritative(true);
35 | $loader->register(true);
36 |
37 | $filesToLoad = \Composer\Autoload\ComposerStaticInita41f68a5b79b4678cf41c80073cd864a::$files;
38 | $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
39 | if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
40 | $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
41 |
42 | require $file;
43 | }
44 | }, null, null);
45 | foreach ($filesToLoad as $fileIdentifier => $file) {
46 | $requireFile($fileIdentifier, $file);
47 | }
48 |
49 | return $loader;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/build/vendor/composer/autoload_static.php:
--------------------------------------------------------------------------------
1 | __DIR__ . '/..' . '/mf2/mf2/Mf2/Parser.php',
11 | 'IndieBlocks_a01125dfebcda7ec3333dcd2d57ad8f2' => __DIR__ . '/../..' . '/../includes/functions.php',
12 | );
13 |
14 | public static $prefixLengthsPsr4 = array (
15 | 'I' =>
16 | array (
17 | 'IndieBlocks\\Mimey\\' => 18,
18 | 'IndieBlocks\\Michelf\\' => 20,
19 | 'IndieBlocks\\Masterminds\\' => 24,
20 | ),
21 | );
22 |
23 | public static $prefixDirsPsr4 = array (
24 | 'IndieBlocks\\Mimey\\' =>
25 | array (
26 | 0 => __DIR__ . '/..' . '/ralouphie/mimey/src',
27 | ),
28 | 'IndieBlocks\\Michelf\\' =>
29 | array (
30 | 0 => __DIR__ . '/..' . '/michelf/php-markdown/Michelf',
31 | ),
32 | 'IndieBlocks\\Masterminds\\' =>
33 | array (
34 | 0 => __DIR__ . '/..' . '/masterminds/html5/src',
35 | ),
36 | );
37 |
38 | public static $classMap = array (
39 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
40 | 'IndieBlocks\\Blocks' => __DIR__ . '/../..' . '/../includes/class-blocks.php',
41 | 'IndieBlocks\\Commands\\Commands' => __DIR__ . '/../..' . '/../includes/commands/class-commands.php',
42 | 'IndieBlocks\\Feeds' => __DIR__ . '/../..' . '/../includes/class-feeds.php',
43 | 'IndieBlocks\\Location' => __DIR__ . '/../..' . '/../includes/class-location.php',
44 | 'IndieBlocks\\Masterminds\\HTML5' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5.php',
45 | 'IndieBlocks\\Masterminds\\HTML5\\Elements' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Elements.php',
46 | 'IndieBlocks\\Masterminds\\HTML5\\Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Entities.php',
47 | 'IndieBlocks\\Masterminds\\HTML5\\Exception' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Exception.php',
48 | 'IndieBlocks\\Masterminds\\HTML5\\InstructionProcessor' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/InstructionProcessor.php',
49 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\CharacterReference' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php',
50 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php',
51 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\EventHandler' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/EventHandler.php',
52 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\FileInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php',
53 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\InputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/InputStream.php',
54 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\ParseError' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/ParseError.php',
55 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\Scanner' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Scanner.php',
56 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\StringInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php',
57 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\Tokenizer' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php',
58 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\TreeBuildingRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php',
59 | 'IndieBlocks\\Masterminds\\HTML5\\Parser\\UTF8Utils' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php',
60 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\HTML5Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php',
61 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\OutputRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php',
62 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\RulesInterface' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php',
63 | 'IndieBlocks\\Masterminds\\HTML5\\Serializer\\Traverser' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/Traverser.php',
64 | 'IndieBlocks\\Michelf\\Markdown' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/Markdown.php',
65 | 'IndieBlocks\\Michelf\\MarkdownExtra' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownExtra.php',
66 | 'IndieBlocks\\Michelf\\MarkdownInterface' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownInterface.php',
67 | 'IndieBlocks\\Micropub_Compat' => __DIR__ . '/../..' . '/../includes/class-micropub-compat.php',
68 | 'IndieBlocks\\Mimey\\MimeMappingGenerator' => __DIR__ . '/..' . '/ralouphie/mimey/src/MimeMappingGenerator.php',
69 | 'IndieBlocks\\Mimey\\MimeTypes' => __DIR__ . '/..' . '/ralouphie/mimey/src/MimeTypes.php',
70 | 'IndieBlocks\\Mimey\\MimeTypesInterface' => __DIR__ . '/..' . '/ralouphie/mimey/src/MimeTypesInterface.php',
71 | 'IndieBlocks\\Options_Handler' => __DIR__ . '/../..' . '/../includes/class-options-handler.php',
72 | 'IndieBlocks\\Parser' => __DIR__ . '/../..' . '/../includes/class-parser.php',
73 | 'IndieBlocks\\Plugin' => __DIR__ . '/../..' . '/../includes/class-plugin.php',
74 | 'IndieBlocks\\Post_Types' => __DIR__ . '/../..' . '/../includes/class-post-types.php',
75 | 'IndieBlocks\\Preview_Cards' => __DIR__ . '/../..' . '/../includes/class-preview-cards.php',
76 | 'IndieBlocks\\Theme_Mf2' => __DIR__ . '/../..' . '/../includes/class-theme-mf2.php',
77 | 'IndieBlocks\\Webmention\\Webmention' => __DIR__ . '/../..' . '/../includes/webmention/class-webmention.php',
78 | 'IndieBlocks\\Webmention\\Webmention_Parser' => __DIR__ . '/../..' . '/../includes/webmention/class-webmention-parser.php',
79 | 'IndieBlocks\\Webmention\\Webmention_Receiver' => __DIR__ . '/../..' . '/../includes/webmention/class-webmention-receiver.php',
80 | 'IndieBlocks\\Webmention\\Webmention_Sender' => __DIR__ . '/../..' . '/../includes/webmention/class-webmention-sender.php',
81 | );
82 |
83 | public static function getInitializer(ClassLoader $loader)
84 | {
85 | return \Closure::bind(function () use ($loader) {
86 | $loader->prefixLengthsPsr4 = ComposerStaticInita41f68a5b79b4678cf41c80073cd864a::$prefixLengthsPsr4;
87 | $loader->prefixDirsPsr4 = ComposerStaticInita41f68a5b79b4678cf41c80073cd864a::$prefixDirsPsr4;
88 | $loader->classMap = ComposerStaticInita41f68a5b79b4678cf41c80073cd864a::$classMap;
89 |
90 | }, null, ClassLoader::class);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/build/vendor/composer/installed.php:
--------------------------------------------------------------------------------
1 | array('name' => '__root__', 'pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => 'd1a34c307a1cb144139314bb49043c6a4bb67106', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \false), 'versions' => array('__root__' => array('pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => 'd1a34c307a1cb144139314bb49043c6a4bb67106', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'masterminds/html5' => array('pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'reference' => 'f5ac2c0b0a2eefca70b2ce32a5809992227e75a6', 'type' => 'library', 'install_path' => __DIR__ . '/../masterminds/html5', 'aliases' => array(), 'dev_requirement' => \false), 'mf2/mf2' => array('pretty_version' => 'v0.5.0', 'version' => '0.5.0.0', 'reference' => 'ddc56de6be62ed4a21f569de9b80e17af678ca50', 'type' => 'library', 'install_path' => __DIR__ . '/../mf2/mf2', 'aliases' => array(), 'dev_requirement' => \false), 'michelf/php-markdown' => array('pretty_version' => '2.0.0', 'version' => '2.0.0.0', 'reference' => 'eb176f173fbac58a045aff78e55f833264b34e71', 'type' => 'library', 'install_path' => __DIR__ . '/../michelf/php-markdown', 'aliases' => array(), 'dev_requirement' => \false), 'ralouphie/mimey' => array('pretty_version' => '1.0.2', 'version' => '1.0.2.0', 'reference' => '2a0e997c733b7c2f9f8b61cafb006fd5fb9fa15a', 'type' => 'library', 'install_path' => __DIR__ . '/../ralouphie/mimey', 'aliases' => array(), 'dev_requirement' => \false)));
6 |
--------------------------------------------------------------------------------
/build/vendor/composer/platform_check.php:
--------------------------------------------------------------------------------
1 | = 70400)) {
8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
9 | }
10 |
11 | if ($issues) {
12 | if (!headers_sent()) {
13 | header('HTTP/1.1 500 Internal Server Error');
14 | }
15 | if (!ini_get('display_errors')) {
16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
18 | } elseif (!headers_sent()) {
19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
20 | }
21 | }
22 | trigger_error(
23 | 'Composer detected issues in your platform: ' . implode(' ', $issues),
24 | E_USER_ERROR
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/build/vendor/masterminds/html5/CREDITS:
--------------------------------------------------------------------------------
1 | Matt Butcher [technosophos] (lead)
2 | Matt Farina [mattfarina] (lead)
3 | Asmir Mustafic [goetas] (contributor)
4 | Edward Z. Yang [ezyang] (contributor)
5 | Geoffrey Sneddon [gsnedders] (contributor)
6 | Kukhar Vasily [ngreduce] (contributor)
7 | Rune Christensen [MrElectronic] (contributor)
8 | Mišo Belica [miso-belica] (contributor)
9 | Asmir Mustafic [goetas] (contributor)
10 | KITAITI Makoto [KitaitiMakoto] (contributor)
11 | Jacob Floyd [cognifloyd] (contributor)
12 |
--------------------------------------------------------------------------------
/build/vendor/masterminds/html5/src/HTML5/Exception.php:
--------------------------------------------------------------------------------
1 | ).
62 | *
63 | * @return int one of the Tokenizer::TEXTMODE_* constants
64 | */
65 | public function startTag($name, $attributes = array(), $selfClosing = \false);
66 | /**
67 | * An end-tag.
68 | */
69 | public function endTag($name);
70 | /**
71 | * A comment section (unparsed character data).
72 | */
73 | public function comment($cdata);
74 | /**
75 | * A unit of parsed character data.
76 | *
77 | * Entities in this text are *already decoded*.
78 | */
79 | public function text($cdata);
80 | /**
81 | * Indicates that the document has been entirely processed.
82 | */
83 | public function eof();
84 | /**
85 | * Emitted when the parser encounters an error condition.
86 | */
87 | public function parseError($msg, $line, $col);
88 | /**
89 | * A CDATA section.
90 | *
91 | * @param string $data
92 | * The unparsed character data
93 | */
94 | public function cdata($data);
95 | /**
96 | * This is a holdover from the XML spec.
97 | *
98 | * While user agents don't get PIs, server-side does.
99 | *
100 | * @param string $name The name of the processor (e.g. 'php').
101 | * @param string $data The unparsed data.
102 | */
103 | public function processingInstruction($name, $data = null);
104 | }
105 |
--------------------------------------------------------------------------------
/build/vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php:
--------------------------------------------------------------------------------
1 | 1, 'dd' => 1, 'dt' => 1, 'rt' => 1, 'rp' => 1, 'tr' => 1, 'th' => 1, 'td' => 1, 'thead' => 1, 'tfoot' => 1, 'tbody' => 1, 'table' => 1, 'optgroup' => 1, 'option' => 1);
20 | /**
21 | * Returns true if the given tagname has special processing rules.
22 | */
23 | public function hasRules($tagname)
24 | {
25 | return isset(static::$tags[$tagname]);
26 | }
27 | /**
28 | * Evaluate the rule for the current tag name.
29 | *
30 | * This may modify the existing DOM.
31 | *
32 | * @return \DOMElement The new Current DOM element.
33 | */
34 | public function evaluate($new, $current)
35 | {
36 | switch ($new->tagName) {
37 | case 'li':
38 | return $this->handleLI($new, $current);
39 | case 'dt':
40 | case 'dd':
41 | return $this->handleDT($new, $current);
42 | case 'rt':
43 | case 'rp':
44 | return $this->handleRT($new, $current);
45 | case 'optgroup':
46 | return $this->closeIfCurrentMatches($new, $current, array('optgroup'));
47 | case 'option':
48 | return $this->closeIfCurrentMatches($new, $current, array('option'));
49 | case 'tr':
50 | return $this->closeIfCurrentMatches($new, $current, array('tr'));
51 | case 'td':
52 | case 'th':
53 | return $this->closeIfCurrentMatches($new, $current, array('th', 'td'));
54 | case 'tbody':
55 | case 'thead':
56 | case 'tfoot':
57 | case 'table':
58 | // Spec isn't explicit about this, but it's necessary.
59 | return $this->closeIfCurrentMatches($new, $current, array('thead', 'tfoot', 'tbody'));
60 | }
61 | return $current;
62 | }
63 | protected function handleLI($ele, $current)
64 | {
65 | return $this->closeIfCurrentMatches($ele, $current, array('li'));
66 | }
67 | protected function handleDT($ele, $current)
68 | {
69 | return $this->closeIfCurrentMatches($ele, $current, array('dt', 'dd'));
70 | }
71 | protected function handleRT($ele, $current)
72 | {
73 | return $this->closeIfCurrentMatches($ele, $current, array('rt', 'rp'));
74 | }
75 | protected function closeIfCurrentMatches($ele, $current, $match)
76 | {
77 | if (\in_array($current->tagName, $match, \true)) {
78 | $current->parentNode->appendChild($ele);
79 | } else {
80 | $current->appendChild($ele);
81 | }
82 | return $ele;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/build/vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php:
--------------------------------------------------------------------------------
1 |
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a
11 | copy of this software and associated documentation files (the
12 | "Software"), to deal in the Software without restriction, including
13 | without limitation the rights to use, copy, modify, merge, publish,
14 | distribute, sublicense, and/or sell copies of the Software, and to
15 | permit persons to whom the Software is furnished to do so, subject to
16 | the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included
19 | in all copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 | */
29 | use IndieBlocks\Masterminds\HTML5\Exception;
30 | /** @internal */
31 | class UTF8Utils
32 | {
33 | /**
34 | * The Unicode replacement character.
35 | */
36 | const FFFD = "�";
37 | /**
38 | * Count the number of characters in a string.
39 | * UTF-8 aware. This will try (in order) iconv, MB, and finally a custom counter.
40 | *
41 | * @param string $string
42 | *
43 | * @return int
44 | */
45 | public static function countChars($string)
46 | {
47 | // Get the length for the string we need.
48 | if (\function_exists('mb_strlen')) {
49 | return \mb_strlen($string, 'utf-8');
50 | }
51 | if (\function_exists('iconv_strlen')) {
52 | return \iconv_strlen($string, 'utf-8');
53 | }
54 | $count = \count_chars($string);
55 | // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
56 | // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
57 | return \array_sum(\array_slice($count, 0, 0x80)) + \array_sum(\array_slice($count, 0xc2, 0x33));
58 | }
59 | /**
60 | * Convert data from the given encoding to UTF-8.
61 | *
62 | * This has not yet been tested with charactersets other than UTF-8.
63 | * It should work with ISO-8859-1/-13 and standard Latin Win charsets.
64 | *
65 | * @param string $data The data to convert
66 | * @param string $encoding A valid encoding. Examples: http://www.php.net/manual/en/mbstring.supported-encodings.php
67 | *
68 | * @return string
69 | */
70 | public static function convertToUTF8($data, $encoding = 'UTF-8')
71 | {
72 | /*
73 | * From the HTML5 spec: Given an encoding, the bytes in the input stream must be converted
74 | * to Unicode characters for the tokeniser, as described by the rules for that encoding,
75 | * except that the leading U+FEFF BYTE ORDER MARK character, if any, must not be stripped
76 | * by the encoding layer (it is stripped by the rule below). Bytes or sequences of bytes
77 | * in the original byte stream that could not be converted to Unicode characters must be
78 | * converted to U+FFFD REPLACEMENT CHARACTER code points.
79 | */
80 | // mb_convert_encoding is chosen over iconv because of a bug. The best
81 | // details for the bug are on http://us1.php.net/manual/en/function.iconv.php#108643
82 | // which contains links to the actual but reports as well as work around
83 | // details.
84 | if (\function_exists('mb_convert_encoding')) {
85 | // mb library has the following behaviors:
86 | // - UTF-16 surrogates result in false.
87 | // - Overlongs and outside Plane 16 result in empty strings.
88 | // Before we run mb_convert_encoding we need to tell it what to do with
89 | // characters it does not know. This could be different than the parent
90 | // application executing this library so we store the value, change it
91 | // to our needs, and then change it back when we are done. This feels
92 | // a little excessive and it would be great if there was a better way.
93 | $save = \mb_substitute_character();
94 | \mb_substitute_character('none');
95 | $data = \mb_convert_encoding($data, 'UTF-8', $encoding);
96 | \mb_substitute_character($save);
97 | } elseif (\function_exists('iconv') && 'auto' !== $encoding) {
98 | // fprintf(STDOUT, "iconv found\n");
99 | // iconv has the following behaviors:
100 | // - Overlong representations are ignored.
101 | // - Beyond Plane 16 is replaced with a lower char.
102 | // - Incomplete sequences generate a warning.
103 | $data = @\iconv($encoding, 'UTF-8//IGNORE', $data);
104 | } else {
105 | throw new Exception('Not implemented, please install mbstring or iconv');
106 | }
107 | /*
108 | * One leading U+FEFF BYTE ORDER MARK character must be ignored if any are present.
109 | */
110 | if ("" === \substr($data, 0, 3)) {
111 | $data = \substr($data, 3);
112 | }
113 | return $data;
114 | }
115 | /**
116 | * Checks for Unicode code points that are not valid in a document.
117 | *
118 | * @param string $data A string to analyze
119 | *
120 | * @return array An array of (string) error messages produced by the scanning
121 | */
122 | public static function checkForIllegalCodepoints($data)
123 | {
124 | // Vestigal error handling.
125 | $errors = array();
126 | /*
127 | * All U+0000 null characters in the input must be replaced by U+FFFD REPLACEMENT CHARACTERs.
128 | * Any occurrences of such characters is a parse error.
129 | */
130 | for ($i = 0, $count = \substr_count($data, "\x00"); $i < $count; ++$i) {
131 | $errors[] = 'null-character';
132 | }
133 | /*
134 | * Any occurrences of any characters in the ranges U+0001 to U+0008, U+000B, U+000E to U+001F, U+007F
135 | * to U+009F, U+D800 to U+DFFF , U+FDD0 to U+FDEF, and characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF,
136 | * U+2FFFE, U+2FFFF, U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, U+6FFFF, U+7FFFE,
137 | * U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF,
138 | * U+DFFFE, U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and U+10FFFF are parse errors.
139 | * (These are all control characters or permanently undefined Unicode characters.)
140 | */
141 | // Check PCRE is loaded.
142 | $count = \preg_match_all('/(?:
143 | [\\x01-\\x08\\x0B\\x0E-\\x1F\\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
144 | |
145 | \\xC2[\\x80-\\x9F] # U+0080 to U+009F
146 | |
147 | \\xED(?:\\xA0[\\x80-\\xFF]|[\\xA1-\\xBE][\\x00-\\xFF]|\\xBF[\\x00-\\xBF]) # U+D800 to U+DFFFF
148 | |
149 | \\xEF\\xB7[\\x90-\\xAF] # U+FDD0 to U+FDEF
150 | |
151 | \\xEF\\xBF[\\xBE\\xBF] # U+FFFE and U+FFFF
152 | |
153 | [\\xF0-\\xF4][\\x8F-\\xBF]\\xBF[\\xBE\\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
154 | )/x', $data, $matches);
155 | for ($i = 0; $i < $count; ++$i) {
156 | $errors[] = 'invalid-codepoint';
157 | }
158 | return $errors;
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/build/vendor/masterminds/html5/src/HTML5/Serializer/RulesInterface.php:
--------------------------------------------------------------------------------
1 | 'html', 'http://www.w3.org/1998/Math/MathML' => 'math', 'http://www.w3.org/2000/svg' => 'svg');
21 | protected $dom;
22 | protected $options;
23 | protected $encode = \false;
24 | protected $rules;
25 | protected $out;
26 | /**
27 | * Create a traverser.
28 | *
29 | * @param \DOMNode|\DOMNodeList $dom The document or node to traverse.
30 | * @param resource $out A stream that allows writing. The traverser will output into this
31 | * stream.
32 | * @param array $options An array of options for the traverser as key/value pairs. These include:
33 | * - encode_entities: A bool to specify if full encding should happen for all named
34 | * charachter references. Defaults to false which escapes &'<>".
35 | * - output_rules: The path to the class handling the output rules.
36 | */
37 | public function __construct($dom, $out, RulesInterface $rules, $options = array())
38 | {
39 | $this->dom = $dom;
40 | $this->out = $out;
41 | $this->rules = $rules;
42 | $this->options = $options;
43 | $this->rules->setTraverser($this);
44 | }
45 | /**
46 | * Tell the traverser to walk the DOM.
47 | *
48 | * @return resource $out Returns the output stream.
49 | */
50 | public function walk()
51 | {
52 | if ($this->dom instanceof \DOMDocument) {
53 | $this->rules->document($this->dom);
54 | } elseif ($this->dom instanceof \DOMDocumentFragment) {
55 | // Document fragments are a special case. Only the children need to
56 | // be serialized.
57 | if ($this->dom->hasChildNodes()) {
58 | $this->children($this->dom->childNodes);
59 | }
60 | } elseif ($this->dom instanceof \DOMNodeList) {
61 | // If this is a NodeList of DOMDocuments this will not work.
62 | $this->children($this->dom);
63 | } else {
64 | $this->node($this->dom);
65 | }
66 | return $this->out;
67 | }
68 | /**
69 | * Process a node in the DOM.
70 | *
71 | * @param mixed $node A node implementing \DOMNode.
72 | */
73 | public function node($node)
74 | {
75 | // A listing of types is at http://php.net/manual/en/dom.constants.php
76 | switch ($node->nodeType) {
77 | case \XML_ELEMENT_NODE:
78 | $this->rules->element($node);
79 | break;
80 | case \XML_TEXT_NODE:
81 | $this->rules->text($node);
82 | break;
83 | case \XML_CDATA_SECTION_NODE:
84 | $this->rules->cdata($node);
85 | break;
86 | case \XML_PI_NODE:
87 | $this->rules->processorInstruction($node);
88 | break;
89 | case \XML_COMMENT_NODE:
90 | $this->rules->comment($node);
91 | break;
92 | // Currently we don't support embedding DTDs.
93 | default:
94 | //print '';
95 | break;
96 | }
97 | }
98 | /**
99 | * Walk through all the nodes on a node list.
100 | *
101 | * @param \DOMNodeList $nl A list of child elements to walk through.
102 | */
103 | public function children($nl)
104 | {
105 | foreach ($nl as $node) {
106 | $this->node($node);
107 | }
108 | }
109 | /**
110 | * Is an element local?
111 | *
112 | * @param mixed $ele An element that implement \DOMNode.
113 | *
114 | * @return bool true if local and false otherwise.
115 | */
116 | public function isLocalElement($ele)
117 | {
118 | $uri = $ele->namespaceURI;
119 | if (empty($uri)) {
120 | return \false;
121 | }
122 | return isset(static::$local_ns[$uri]);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/build/vendor/michelf/php-markdown/Michelf/Markdown.inc.php:
--------------------------------------------------------------------------------
1 |
8 | * @copyright 2004-2022 Michel Fortin
9 | * @copyright (Original Markdown) 2004-2006 John Gruber
10 | */
11 | namespace IndieBlocks\Michelf;
12 |
13 | /**
14 | * Markdown Parser Interface
15 | * @internal
16 | */
17 | interface MarkdownInterface
18 | {
19 | /**
20 | * Initialize the parser and return the result of its transform method.
21 | * This will work fine for derived classes too.
22 | *
23 | * @api
24 | *
25 | * @param string $text
26 | * @return string
27 | */
28 | public static function defaultTransform(string $text) : string;
29 | /**
30 | * Main function. Performs some preprocessing on the input text
31 | * and pass it through the document gamut.
32 | *
33 | * @api
34 | *
35 | * @param string $text
36 | * @return string
37 | */
38 | public function transform(string $text) : string;
39 | }
40 |
--------------------------------------------------------------------------------
/build/vendor/michelf/php-markdown/Readme.php:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 | PHP Markdown Lib - Readme
24 |
25 |
26 |
30 |
31 |
32 | mime_types_text = $mime_types_text;
22 | }
23 | /**
24 | * Read the given mime.types text and return a mapping compatible with the MimeTypes class.
25 | *
26 | * @return array The mapping.
27 | */
28 | public function generateMapping()
29 | {
30 | $mapping = array();
31 | $lines = \explode("\n", $this->mime_types_text);
32 | foreach ($lines as $line) {
33 | $line = \trim(\preg_replace('~\\#.*~', '', $line));
34 | $parts = $line ? \array_values(\array_filter(\explode("\t", $line))) : array();
35 | if (\count($parts) === 2) {
36 | $mime = \trim($parts[0]);
37 | $extensions = \explode(' ', $parts[1]);
38 | foreach ($extensions as $extension) {
39 | $extension = \trim($extension);
40 | if ($mime && $extension) {
41 | $mapping['mimes'][$extension][] = $mime;
42 | $mapping['extensions'][$mime][] = $extension;
43 | }
44 | }
45 | }
46 | }
47 | return $mapping;
48 | }
49 | /**
50 | * Read the given mime.types text and generate mapping code.
51 | *
52 | * @return string The mapping PHP code for inclusion.
53 | */
54 | public function generateMappingCode()
55 | {
56 | $mapping = $this->generateMapping();
57 | $mapping_export = \var_export($mapping, \true);
58 | return "
25 | * array(
26 | * 'mimes' => array(
27 | * 'application/json' => array('json'),
28 | * 'image/jpeg' => array('jpg', 'jpeg'),
29 | * ...
30 | * ),
31 | * 'extensions' => array(
32 | * 'json' => array('application/json'),
33 | * 'jpeg' => array('image/jpeg'),
34 | * ...
35 | * )
36 | * )
37 | *
38 | */
39 | public function __construct($mapping = null)
40 | {
41 | if ($mapping === null) {
42 | if (self::$built_in === null) {
43 | self::$built_in = (require \dirname(__DIR__) . '/mime.types.php');
44 | }
45 | $this->mapping = self::$built_in;
46 | } else {
47 | $this->mapping = $mapping;
48 | }
49 | }
50 | /**
51 | * @inheritdoc
52 | */
53 | public function getMimeType($extension)
54 | {
55 | $extension = $this->cleanInput($extension);
56 | if (!empty($this->mapping['mimes'][$extension])) {
57 | return $this->mapping['mimes'][$extension][0];
58 | }
59 | return null;
60 | }
61 | /**
62 | * @inheritdoc
63 | */
64 | public function getExtension($mime_type)
65 | {
66 | $mime_type = $this->cleanInput($mime_type);
67 | if (!empty($this->mapping['extensions'][$mime_type])) {
68 | return $this->mapping['extensions'][$mime_type][0];
69 | }
70 | return null;
71 | }
72 | /**
73 | * @inheritdoc
74 | */
75 | public function getAllMimeTypes($extension)
76 | {
77 | $extension = $this->cleanInput($extension);
78 | if (isset($this->mapping['mimes'][$extension])) {
79 | return $this->mapping['mimes'][$extension];
80 | }
81 | return array();
82 | }
83 | /**
84 | * @inheritdoc
85 | */
86 | public function getAllExtensions($mime_type)
87 | {
88 | $mime_type = $this->cleanInput($mime_type);
89 | if (isset($this->mapping['extensions'][$mime_type])) {
90 | return $this->mapping['extensions'][$mime_type];
91 | }
92 | return array();
93 | }
94 | /**
95 | * Normalize the input string using lowercase/trim.
96 | *
97 | * @param $input The string to normalize.
98 | *
99 | * @return string The normalized string.
100 | */
101 | private function cleanInput($input)
102 | {
103 | return \strtolower(\trim($input));
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/build/vendor/ralouphie/mimey/src/MimeTypesInterface.php:
--------------------------------------------------------------------------------
1 | request ) && 0 === strpos( $wp->request, $front . '/feed' ) ) {
128 | /* translators: %s: site title */
129 | $title = sprintf( __( 'Posts – %s', 'indieblocks' ), get_bloginfo( 'title' ) );
130 | $title = apply_filters( 'indieblocks_post_feed_title', $title );
131 | }
132 |
133 | return $title;
134 | }
135 |
136 | /**
137 | * Adds a `link`, in `head`, to the newly created post feed.
138 | */
139 | public static function add_post_feed_link() {
140 | $front = static::get_front();
141 |
142 | if ( empty( $front ) ) {
143 | // Do nothing.
144 | return;
145 | }
146 |
147 | /* translators: %s: site title */
148 | $title = sprintf( __( 'Posts – %s', 'indieblocks' ), get_bloginfo( 'title' ) );
149 | $title = apply_filters( 'indieblocks_post_feed_title', $title );
150 |
151 | $feed_url = home_url( "$front/feed/" );
152 |
153 | $permalink_structure = get_option( 'permalink_structure' );
154 | if ( is_string( $permalink_structure ) && '/' !== substr( $permalink_structure, -1 ) ) {
155 | // If permalinks were set up without trailing slash, hide it.
156 | $feed_url = substr( $feed_url, 0, -1 );
157 | }
158 |
159 | echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
160 | }
161 |
162 | /**
163 | * Prepends Featured Images to RSS items.
164 | *
165 | * @param string $content The post content.
166 | * @return string Modified content.
167 | */
168 | public static function feed_thumbnails( $content ) {
169 | global $post;
170 |
171 | if ( ! empty( $post->ID ) && has_post_thumbnail( $post->ID ) ) {
172 | $content = '' . get_the_post_thumbnail( $post->ID ) . '
' . PHP_EOL . $content;
173 | }
174 |
175 | return $content;
176 | }
177 |
178 | /**
179 | * Returns the permalink structure's "front," if any.
180 | *
181 | * @return string The permalink front, without slashes, or an empty string.
182 | */
183 | public static function get_front() {
184 | global $wp_rewrite;
185 |
186 | return isset( $wp_rewrite->front ) ? trim( $wp_rewrite->front, '/' ) : '';
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/includes/class-plugin.php:
--------------------------------------------------------------------------------
1 | options_handler = new Options_Handler();
67 | $this->options_handler->register();
68 |
69 | // Load "modules." We hook these up to `plugins_loaded` rather than
70 | // directly call the `register()` methods. This allows other plugins to
71 | // more easily unhook them.
72 | $options = $this->options_handler->get_options();
73 |
74 | if ( ! empty( $options['webmention'] ) ) {
75 | add_action( 'plugins_loaded', array( Webmention::class, 'register' ) );
76 | }
77 |
78 | // Gutenberg blocks.
79 | if ( ! empty( $options['enable_blocks'] ) ) {
80 | add_action( 'plugins_loaded', array( Blocks::class, 'register' ) );
81 | }
82 |
83 | // `Feeds::register()` runs its own option check.
84 | add_action( 'plugins_loaded', array( Feeds::class, 'register' ) );
85 |
86 | // Location and weather functions.
87 | if ( ! empty( $options['location_functions'] ) ) {
88 | add_action( 'plugins_loaded', array( Location::class, 'register' ) );
89 | }
90 |
91 | // Custom Post Types.
92 | add_action( 'plugins_loaded', array( Post_Types::class, 'register' ) );
93 |
94 | // Everything Site Editor/theme microformats.
95 | if ( $this->theme_supports_blocks() ) {
96 | add_filter( 'pre_get_avatar', array( Theme_Mf2::class, 'get_avatar_html' ), 10, 3 );
97 | add_action( 'admin_enqueue_scripts', array( Webmention::class, 'enqueue_styles' ), 10, 3 );
98 |
99 | if ( ! empty( $options['add_mf2'] ) ) {
100 | add_action( 'plugins_loaded', array( Theme_Mf2::class, 'register' ) );
101 | }
102 | }
103 |
104 | // Micropub hook callbacks.
105 | add_action( 'plugins_loaded', array( Micropub_Compat::class, 'register' ) );
106 |
107 | // Link preview cards.
108 | if ( ! empty( $options['preview_cards'] ) ) {
109 | add_action( 'plugins_loaded', array( Preview_Cards::class, 'register' ) );
110 | }
111 | }
112 |
113 | /**
114 | * Registers permalinks on activation.
115 | *
116 | * We flush permalinks every time the post types or feed options are
117 | * changed, and each time the plugin is (de)activated.
118 | */
119 | public function activate() {
120 | flush_permalinks();
121 | }
122 |
123 | /**
124 | * Deschedules the Webmention cron job, and resets permalinks on plugin
125 | * deactivation.
126 | */
127 | public function deactivate() {
128 | Webmention::deactivate();
129 | flush_rewrite_rules();
130 | }
131 |
132 | /**
133 | * Enable i18n.
134 | */
135 | public function load_textdomain() {
136 | load_plugin_textdomain( 'indieblocks', false, basename( dirname( __DIR__ ) ) . '/languages' );
137 | }
138 |
139 | /**
140 | * Returns our options handler.
141 | *
142 | * @return Options_Handler Options handler.
143 | */
144 | public function get_options_handler() {
145 | return $this->options_handler;
146 | }
147 |
148 | /**
149 | * Whether the active theme supports blocks.
150 | */
151 | protected function theme_supports_blocks() {
152 | return is_readable( get_template_directory() . '/templates/index.html' ) || current_theme_supports( 'add_block_template_part_support' );
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/includes/class-preview-cards.php:
--------------------------------------------------------------------------------
1 | ID ) );
41 | }
42 |
43 | /**
44 | * Saves preview card metadata as custom fields.
45 | *
46 | * @param int $post_id Post ID.
47 | */
48 | public static function add_meta( $post_id ) {
49 | $post = get_post( $post_id );
50 |
51 | if ( empty( $post->post_content ) ) {
52 | return;
53 | }
54 |
55 | $parser = post_content_parser( $post );
56 | $linked_url = $parser->get_link_url( false ); // Get the first link, regardless of microformats.
57 |
58 | if ( empty( $linked_url ) ) {
59 | delete_post_meta( $post_id, '_indieblocks_link_preview' );
60 | return;
61 | }
62 |
63 | $parser = new Parser( $linked_url );
64 | $parser->parse();
65 |
66 | $name = $parser->get_name( false ); // Also consider non-mf2 titles, e.g., for notes.
67 | if ( '' === $name ) {
68 | delete_post_meta( $post_id, '_indieblocks_link_preview' );
69 | return;
70 | }
71 |
72 | $thumbnail = '';
73 | $image = $parser->get_image();
74 | if ( '' !== $image ) {
75 | $thumbnail = static::create_thumbnail( $image, $post );
76 | }
77 |
78 | $meta = array_filter( // Remove empty values.
79 | array(
80 | 'title' => $name,
81 | 'url' => $linked_url,
82 | 'thumbnail' => $thumbnail,
83 | )
84 | );
85 | update_post_meta( $post->ID, '_indieblocks_link_preview', $meta );
86 | }
87 |
88 | /**
89 | * Creates a link preview thumbnail and returns its local URL.
90 | *
91 | * @param string $url Image URL.
92 | * @return string Local thumbnail URL.
93 | */
94 | protected static function create_thumbnail( $url ) {
95 | $dir = 'indieblocks-cards';
96 |
97 | $upload_dir = wp_upload_dir();
98 | if ( ! empty( $upload_dir['subdir'] ) ) {
99 | // Add month and year, to be able to keep track of things.
100 | $dir .= '/' . trim( $upload_dir['subdir'], '/' );
101 | }
102 |
103 | $hash = hash( 'sha256', esc_url_raw( $url ) );
104 | $ext = pathinfo( $url, PATHINFO_EXTENSION );
105 | $filename = $hash . ( ! empty( $ext ) ? '.' . $ext : '' );
106 |
107 | return store_image( $url, $filename, $dir );
108 | }
109 |
110 | /**
111 | * Registers a custom REST API endpoint for reading (but not writing) our
112 | * location data.
113 | */
114 | public static function register_rest_field() {
115 | foreach ( array( 'post', 'indieblocks_note', 'indieblocks_like' ) as $post_type ) {
116 | register_rest_field(
117 | $post_type,
118 | 'indieblocks_link_preview',
119 | array(
120 | 'get_callback' => array( __CLASS__, 'get_meta' ),
121 | 'update_callback' => null,
122 | )
123 | );
124 | }
125 | }
126 |
127 | /**
128 | * Returns link preview metadata.
129 | *
130 | * @param array $params WP REST API request.
131 | * @return mixed Response.
132 | */
133 | public static function get_meta( $params ) {
134 | $post_id = $params['id'];
135 |
136 | if ( empty( $post_id ) || ! is_int( $post_id ) ) {
137 | return new \WP_Error( 'invalid_id', 'Invalid post ID.', array( 'status' => 400 ) );
138 | }
139 |
140 | return get_post_meta( (int) $post_id, '_indieblocks_link_preview', true ); // Either an empty string, or an associated array (which gets translated into a JSON object).
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/includes/commands/class-commands.php:
--------------------------------------------------------------------------------
1 |
17 | * : The comment ID.
18 | *
19 | * [--w=]
20 | * : The target width.
21 | *
22 | * [--h=]
23 | * : The target height.
24 | *
25 | * [--dir=]
26 | * : The destination folder, relative to, typically, `wp-content/uploads`.
27 | *
28 | * [--author=]
29 | * : Author URL.
30 | *
31 | * [--type=]
32 | * : `ap` for ActivityPub.
33 | *
34 | * @subcommand cache-avatar
35 | *
36 | * @param array $args Arguments.
37 | * @param array $assoc_args "Associated" arguments.
38 | */
39 | public function cache_avatar( $args, $assoc_args ) {
40 | $options = get_options();
41 | if ( empty( $options['cache_avatars'] ) ) {
42 | \WP_CLI::error( 'Avatar cache disabled.' );
43 | return;
44 | }
45 |
46 | $comment_id = trim( $args[0] );
47 | $comment = get_comment( $comment_id );
48 |
49 | if ( ! $comment instanceof \WP_Comment ) {
50 | \WP_CLI::error( 'Invalid comment.' );
51 | return;
52 | }
53 |
54 | if ( isset( $assoc_args['type'] ) && 'ap' === $assoc_args['type'] ) {
55 | // This is a special case, to locally save avatars stored by the
56 | // ActivityPub plugin.
57 | $url = get_meta( $comment, 'avatar_url' );
58 | $dir = 'activitypub-avatars';
59 | } elseif ( isset( $assoc_args['type'] ) && 'wm' === $assoc_args['type'] ) {
60 | // This is a special case, to locally save avatars stored by the
61 | // ActivityPub plugin.
62 | $url = get_meta( $comment, 'avatar' );
63 | $dir = 'indieblocks-avatars'; // We're okay reusing this folder.
64 | } else {
65 | $url = get_meta( $comment, 'indieblocks_webmention_avatar' );
66 | $dir = isset( $assoc_args['dir'] )
67 | ? $assoc_args['dir']
68 | : 'indieblocks-avatars';
69 | $dir = sanitize_title( $dir );
70 | }
71 |
72 | if ( empty( $url ) ) {
73 | \WP_CLI::error( 'Invalid avatar URL.' );
74 | return;
75 | }
76 |
77 | if ( isset( $assoc_args['author'] ) && filter_var( $assoc_args['author'], FILTER_VALIDATE_URL ) ) {
78 | $hash = hash( 'sha256', esc_url_raw( $assoc_args['author'] ) );
79 | } else {
80 | $hash = hash( 'sha256', esc_url_raw( $url ) );
81 | }
82 |
83 | $dir .= '/' . substr( $hash, 0, 2 ) . '/' . substr( $hash, 2, 2 );
84 |
85 | $ext = pathinfo( $url, PATHINFO_EXTENSION );
86 | $filename = $hash . ( ! empty( $ext ) ? '.' . $ext : '' );
87 |
88 | \WP_CLI::log( "Saving to `$dir/$filename`." );
89 |
90 | $width = isset( $assoc_args['w'] ) && ctype_digit( (string) $assoc_args['w'] )
91 | ? (int) $assoc_args['w']
92 | : 150;
93 |
94 | $height = isset( $assoc_args['h'] ) && ctype_digit( (string) $assoc_args['h'] )
95 | ? (int) $assoc_args['h']
96 | : 150;
97 |
98 | $result = store_image( $url, $filename, $dir, $width, $height );
99 |
100 | if ( $result ) {
101 | if ( isset( $assoc_args['type'] ) && 'ap' === $assoc_args['type'] ) {
102 | // That special case again.
103 | update_meta( $comment, 'avatar_url', $result );
104 | } elseif ( isset( $assoc_args['type'] ) && 'wm' === $assoc_args['type'] ) {
105 | update_meta( $comment, 'avatar', $result );
106 | } else {
107 | update_meta( $comment, 'indieblocks_webmention_avatar', $result );
108 | }
109 |
110 | \WP_CLI::success( 'All done!' );
111 | } else {
112 | \WP_CLI::error( 'Something went wrong.' );
113 | }
114 | }
115 |
116 | /**
117 | * Downloads and resizes an image.
118 | *
119 | * ## OPTIONS
120 | *
121 | *
122 | * : The URL of the image.
123 | *
124 | * [--w=]
125 | * : The target width.
126 | *
127 | * [--h=]
128 | * : The target height.
129 | *
130 | * [--dir=]
131 | * : The destination folder, relative to, typically, `wp-content/uploads`.
132 | *
133 | * [--author=]
134 | * : Author URL.
135 | *
136 | * @subcommand cache-image
137 | *
138 | * @param array $args Arguments.
139 | * @param array $assoc_args "Associated" arguments.
140 | */
141 | public function cache_image( $args, $assoc_args ) {
142 | $options = get_options();
143 | if ( empty( $options['cache_avatars'] ) ) {
144 | \WP_CLI::error( 'Avatar cache disabled.' );
145 | return;
146 | }
147 |
148 | $url = trim( $args[0] );
149 | if ( ! preg_match( '~^https?://~', $url ) ) {
150 | $url = 'http://' . ltrim( $url, '/' );
151 | }
152 |
153 | if ( ! wp_http_validate_url( $url ) ) {
154 | \WP_CLI::error( 'Invalid URL.' );
155 | return;
156 | }
157 |
158 | $dir = isset( $assoc_args['dir'] )
159 | ? $assoc_args['dir']
160 | : 'indieblocks-avatars';
161 | $dir = sanitize_title( $dir );
162 |
163 | if ( isset( $assoc_args['author'] ) && filter_var( $assoc_args['author'], FILTER_VALIDATE_URL ) ) {
164 | $hash = hash( 'sha256', esc_url_raw( $assoc_args['author'] ) );
165 | } else {
166 | $hash = hash( 'sha256', esc_url_raw( $url ) );
167 | }
168 |
169 | $dir .= '/' . substr( $hash, 0, 2 ) . '/' . substr( $hash, 2, 2 );
170 |
171 | $ext = pathinfo( $url, PATHINFO_EXTENSION );
172 | $filename = $hash . ( ! empty( $ext ) ? '.' . $ext : '' );
173 |
174 | \WP_CLI::log( "Saving to `$dir/$filename`." );
175 |
176 | $width = isset( $assoc_args['w'] ) && ctype_digit( (string) $assoc_args['w'] )
177 | ? (int) $assoc_args['w']
178 | : 150;
179 |
180 | $height = isset( $assoc_args['h'] ) && ctype_digit( (string) $assoc_args['h'] )
181 | ? (int) $assoc_args['h']
182 | : 150;
183 |
184 | $result = store_image( $url, $filename, $dir, $width, $height );
185 |
186 | if ( $result ) {
187 | \WP_CLI::success( 'All done!' );
188 | } else {
189 | \WP_CLI::error( 'Something went wrong.' );
190 | }
191 | }
192 |
193 | /**
194 | * Deletes an avatar and all references to it.
195 | *
196 | * ## OPTIONS
197 | *
198 | *
199 | * : The (local) image URL.
200 | *
201 | * [--key=]
202 | * : The comment meta key that holds the (local) URL.
203 | *
204 | * @subcommand delete-image
205 | *
206 | * @param array $args Arguments.
207 | * @param array $assoc_args "Associated" arguments.
208 | */
209 | public function delete_image( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
210 | $url = trim( $args[0] );
211 | if ( ! preg_match( '~^https?://~', $url ) ) {
212 | $url = 'http://' . ltrim( $url, '/' );
213 | }
214 |
215 | if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
216 | \WP_CLI::error( 'Invalid URL.' );
217 | return;
218 | }
219 |
220 | $key = isset( $assoc_args['key'] )
221 | ? $assoc_args['key']
222 | : 'indieblocks_webmention_avatar';
223 |
224 | $upload_dir = wp_upload_dir();
225 | $file_path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $url );
226 |
227 | // Delete file.
228 | wp_delete_file( $file_path );
229 |
230 | // Delete all references to _this_ file.
231 | $result = delete_metadata(
232 | 'comment',
233 | 0,
234 | $key,
235 | esc_url_raw( $url ),
236 | true // Delete matching metadata entries for all objects.
237 | );
238 |
239 | if ( $result ) {
240 | \WP_CLI::success( 'All done!' );
241 | } else {
242 | \WP_CLI::error( 'Something went wrong.' );
243 | }
244 | }
245 |
246 | /**
247 | * Deletes a link preview card.
248 | *
249 | * ## OPTIONS
250 | *
251 | *
252 | * : The post ID.
253 | *
254 | * @subcommand delete-link-preview
255 | *
256 | * @param array $args Arguments.
257 | * @param array $assoc_args "Associated" arguments.
258 | */
259 | public function delete_link_preview( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
260 | $id = trim( $args[0] );
261 | if ( ! ctype_digit( (string) $id ) ) {
262 | \WP_CLI::error( 'Invalid ID.' );
263 | return;
264 | }
265 |
266 | $post = get_post( $id );
267 | if ( empty( $post ) ) {
268 | \WP_CLI::error( 'Invalid ID.' );
269 | return;
270 | }
271 |
272 | $card = get_post_meta( $post->ID, '_indieblocks_link_preview', true );
273 | if ( ! empty( $card['thumbnail'] ) ) {
274 | $upload_dir = wp_upload_dir();
275 | $file_path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $card['thumbnail'] );
276 |
277 | \WP_CLI::log( 'Deleting link preview thumbnail.' );
278 |
279 | wp_delete_file( $file_path );
280 | }
281 |
282 | $result = delete_post_meta( $post->ID, '_indieblocks_link_preview' );
283 |
284 | if ( $result ) {
285 | \WP_CLI::success( 'All done!' );
286 | } else {
287 | \WP_CLI::error( 'Something went wrong.' );
288 | }
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/indieblocks.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {
8 | "files.exclude": {
9 | "**/docs/**": true,
10 | "**/svn/**": true,
11 | "vendor/**": true,
12 | ".phpunit.result.cache": true,
13 | },
14 | "files.watcherExclude": {
15 | "**/docs/**": true,
16 | "**/svn/**": true,
17 | "**/vendor/**": true,
18 | ".phpunit.result.cache": true,
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/indieblocks.php:
--------------------------------------------------------------------------------
1 |
17 | * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
18 | * @package IndieBlocks
19 | */
20 |
21 | namespace IndieBlocks;
22 |
23 | // Prevent direct access.
24 | if ( ! defined( 'ABSPATH' ) ) {
25 | exit;
26 | }
27 |
28 | // Load dependencies.
29 | require_once __DIR__ . '/build/vendor/scoper-autoload.php';
30 |
31 | $indieblocks = Plugin::get_instance();
32 | $indieblocks->register();
33 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | *\.asset\.php
16 |
17 |
18 | tests/*
19 | build/*
20 | bootstrap\.php$
21 | scoper\.inc\.php$
22 | *\.(css|js)
23 |
24 |
25 |
26 | *\.php$
27 |
28 |
29 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === IndieBlocks ===
2 | Contributors: janboddez
3 | Tags: blocks, indieweb, notes, microblog, webmention
4 | Tested up to: 6.7
5 | Stable tag: 0.13.2
6 | License: GNU General Public License v3.0
7 | License URI: https://www.gnu.org/licenses/gpl-3.0.html
8 |
9 | Use blocks, and, optionally, "short-form" post types to easily "IndieWebify" your WordPress site.
10 |
11 | == Description ==
12 | Use blocks, and, optionally, "short-form" post types to easily "IndieWebify" your WordPress site.
13 |
14 | IndieBlocks registers several blocks (Bookmark, Like, Reply, and Repost, as well as the older Context block) that take a URL and output corresponding _microformatted_ HTML.
15 |
16 | In combination with a microformats-compatible theme, these help ensure microformats clients are able to determine a post's type.
17 |
18 | It also comes with "short-form" (Note and Like) custom post types, and a (somewhat experimental) option to add microformats to (all!) *block-based* themes.
19 |
20 | These microformats, in combination with the Webmention protocol, allow for rich _cross-site_ conversations. IndieBlocks comes with its own Webmention implementation, but a separate plugin can be used, too.
21 |
22 | IndieBlocks also registers several "theme" blocks (Facepile, Location, Syndication, and Link Preview), to be used in "block theme" templates.
23 |
24 | == Installation ==
25 | Upload this plugin's ZIP file via the Plugins > Add New > "Upload Plugin" button.
26 |
27 | After activation, head over to *Settings > IndieBlocks*, and enable or disable its different features.
28 |
29 | More details can be found on [https://indieblocks.xyz/](https://indieblocks.xyz/). Issues may be filed at [https://github.com/janboddez/indieblocks](https://github.com/janboddez/indieblocks).
30 |
31 | == Frequently Asked Questions ==
32 | = How does this plugin interact with the various other IndieWeb plugins? =
33 | While IndieBlocks does not depend on _any_ other plugin, it is compatible with, and extends, the Micropub plugin for WordPress. See [https://indieblocks.xyz/documentation/micropub-and-indieauth/](https://indieblocks.xyz/documentation/micropub-and-indieauth/) for some more information.
34 |
35 | IndieBlocks’ Facepile and Syndication blocks also aim to be compatible with, respectively, the Webmention and Syndication Links plugins.
36 |
37 | == Changelog ==
38 | = 0.13.2 =
39 | Removed "image proxy."
40 |
41 | = 0.13.1 =
42 | Minor bug fixes. Improved "Facepile" compatibility (with the ActivityPub plugin).
43 |
44 | = 0.13.0 =
45 | Improve Gutenberg compatibility of Location and Webmention "meta boxes." Add Syndication block prefix and suffix attributes. Support "update" and "delete" webmentions even after mentions are closed. Add avatar proxy option.
46 |
47 | = 0.12.0 =
48 | Improve comment mentions, remove margin "below" hidden note and like titles.
49 |
50 | = 0.11.0 =
51 | Improve avatar deletion, add meta box for outgoing "comment mentions," hide meta boxes if empty.
52 |
53 | = 0.10.0 =
54 | Send webmentions also for comments, to mentioned sites and the comment parent, if it exists and itself originated as a webmention.
55 |
56 | = 0.9.1 =
57 | Fix Webmention backlinks in Facepile block, add avatar background and icon color pickers.
58 |
59 | = 0.9.0 =
60 | Overhaul theme microformats functionality.
61 |
62 | = 0.8.1 =
63 | Fix issue with saving meta from block editor. Fix Markdown in Micropub notes.
64 |
65 | = 0.8.0 =
66 | Various bug fixes. Add Link Preview block. Also, webmentions are now closed when comments are, although this behavior is filterable.
67 |
68 | = 0.7.1 =
69 | Add Location block. The Facepile block now supports v5.0 and up of the Webmention plugin.
70 |
71 | = 0.7.0 =
72 | Store temperatures in Kelvin rather than degrees Celsius. Update `masterminds/html5` to version 2.8.0. Add Location block.
73 |
74 | = 0.6.0 =
75 | "Facepile" likes, bookmarks, and reposts.
76 |
77 | = 0.5.0 =
78 | Add Bookmark, Like, Reply and Repost blocks. Additional title options.
79 |
80 | = 0.4.0 =
81 | Add `indieblocks/syndication-links` block.
82 |
83 | = 0.3.6 =
84 | Minor bug fix, new plugin URL.
85 |
86 | = 0.3.5 =
87 | Fix rescheduling of webmentions from the classic editor.
88 |
89 | = 0.3.4 =
90 | Webmention tweaks.
91 |
92 | = 0.3.3 =
93 | Slight block changes. Bug fixes, and basic Webmention support.
94 |
95 | = 0.2.0 =
96 | Slightly improved "empty" URL handling, and permalink flushing. Additional CPT, feed and Micropub options. Date-based CPT archives, and basic location functions.
97 |
--------------------------------------------------------------------------------
/scoper.inc.php:
--------------------------------------------------------------------------------
1 | 'IndieBlocks',
9 | 'output-dir' => 'build',
10 | // For more see: https://github.com/humbug/php-scoper#finders-and-paths
11 | 'finders' => [
12 | Finder::create()
13 | ->files()
14 | ->ignoreVCS(true)
15 | ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock|phpunit/')
16 | ->exclude([
17 | 'doc',
18 | 'test',
19 | 'test_old',
20 | 'tests',
21 | 'Tests',
22 | 'vendor-bin',
23 | ])
24 | ->in('vendor'),
25 | Finder::create()->append([
26 | 'composer.json',
27 | ]),
28 | ]
29 | ];
30 |
--------------------------------------------------------------------------------
/scoper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | php-scoper add-prefix -f
3 | sed -i "s|blocks|..\\\/blocks|g" build/composer.json
4 | sed -i "s|includes|..\\\/includes|g" build/composer.json
5 | composer dump-autoload --working-dir build --classmap-authoritative
6 | find ./build/vendor -type d -name bin -prune -exec rm -rf {} \;
7 | # find ./build/vendor -type f ! -name "*.php" | xargs rm
8 | find ./build/vendor -type f \( -name "*.json" -o -name "*.xml" \) | xargs rm
9 |
--------------------------------------------------------------------------------
/templates/feed-atom.php:
--------------------------------------------------------------------------------
1 | ';
18 |
19 | /** This action is documented in wp-includes/feed-rss2.php */
20 | do_action( 'rss_tag_pre', 'atom' );
21 | ?>
22 |
34 | >
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
55 |
56 |
57 |
58 |
62 |
63 |
73 |
74 |
75 |
81 | ]]>
82 |
87 | ]]>
88 |
93 | ]]>
94 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | ]]>
106 |
107 |
108 | ]]>
109 |
110 |
111 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | ';
18 |
19 | /**
20 | * Fires between the xml and rss tags in a feed.
21 | *
22 | * @since 4.0.0
23 | *
24 | * @param string $context Type of feed. Possible values include 'rss2', 'rss2-comments',
25 | * 'rdf', 'atom', and 'atom-comments'.
26 | */
27 | do_action( 'rss_tag_pre', 'rss2' );
28 | ?>
29 |
44 | >
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
67 |
68 |
69 |
82 |
83 |
94 | -
95 |
101 | ]]>
102 |
107 | ]]>
108 |
113 | ]]>
114 |
118 |
119 |
120 |
121 |
122 |
123 | ]]>
124 |
125 |
126 |
127 |
128 |
129 | ]]>
130 |
131 | ]]>
132 |
133 | 0 ) : ?>
134 | ]]>
135 |
136 | ]]>
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
155 |
156 |
157 |
158 |
159 |