diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index fe686e7..36cc8bd 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -30,7 +30,7 @@ jobs: php-versions: ['7.4', '8.3', '8.4'] include: - wp-version: latest - - wp-version: '6.3' + - wp-version: '6.8' php-versions: '7.4' - wp-version: trunk php-versions: '8.4' diff --git a/blockroll.php b/blockroll.php index d3ed139..09996ca 100644 --- a/blockroll.php +++ b/blockroll.php @@ -3,8 +3,8 @@ * Plugin Name: Blogroll & Podroll Block * Plugin URI: https://github.com/pfefferle/wordpress-blockroll * Description: Share the blogs and podcasts you follow, and let other people subscribe to your list. - * Version: 1.2.0 - * Requires at least: 6.5 + * Version: 2.0.0 + * Requires at least: 6.8 * Requires PHP: 7.4 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ @@ -46,7 +46,10 @@ function init() { Anchors::register(); Opml::register(); Xfn::register(); - \register_block_type( __DIR__ . '/build/blogroll' ); + // The blogroll renders its list itself, from the attributes of its link + // blocks; the link blocks have no output, so core need not render them. + \register_block_type( __DIR__ . '/build/blogroll', array( 'skip_inner_blocks' => true ) ); + \register_block_type( __DIR__ . '/build/link' ); } \add_action( 'init', __NAMESPACE__ . '\init' ); diff --git a/build/blogroll/block.json b/build/blogroll/block.json index db82671..59d42bb 100644 --- a/build/blogroll/block.json +++ b/build/blogroll/block.json @@ -4,7 +4,6 @@ "name": "blockroll/blogroll", "title": "Blogroll", "category": "widgets", - "icon": "admin-links", "description": "Share a list of the blogs and sites you follow.", "textdomain": "blockroll", "keywords": [ @@ -70,6 +69,9 @@ "default": true } }, + "providesContext": { + "blockroll/showAvatars": "showAvatars" + }, "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", diff --git a/build/blogroll/index-rtl.css b/build/blogroll/index-rtl.css index 2ace2ae..d3caefa 100644 --- a/build/blogroll/index-rtl.css +++ b/build/blogroll/index-rtl.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.blockroll-add-link .components-popover__content{max-width:90vw;padding:8px;width:360px}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-start;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__row .has-message .components-base-control__help{color:#cc1818}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 37c9d47..440638e 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'c3dd04e9224b2aa7b893'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '0006d4a7c1c4a6beeab5'); diff --git a/build/blogroll/index.css b/build/blogroll/index.css index 2ace2ae..d3caefa 100644 --- a/build/blogroll/index.css +++ b/build/blogroll/index.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.blockroll-add-link .components-popover__content{max-width:90vw;padding:8px;width:360px}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-start;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__row .has-message .components-base-control__help{color:#cc1818}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} diff --git a/build/blogroll/index.js b/build/blogroll/index.js index f39538d..be0d2a2 100644 --- a/build/blogroll/index.js +++ b/build/blogroll/index.js @@ -1,6 +1,6 @@ -(()=>{"use strict";var e={408(e,l,o){const t=window.wp.blocks,n=JSON.parse('{"UU":"blockroll/blogroll"}'),r=window.wp.i18n,s=window.wp.element,a=window.wp.data,i=window.wp.blockEditor,c=window.wp.components,d=window.wp.primitives,u=window.ReactJSXRuntime;var h=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"M12 3.9 6.5 9.5l1 1 3.8-3.7V20h1.5V6.8l3.7 3.7 1-1z"})}),_=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"m16.5 13.5-3.7 3.7V4h-1.5v13.2l-3.8-3.7-1 1 5.5 5.6 5.5-5.6z"})}),p=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),m=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})});const b=window.wp.apiFetch;var x=o.n(b);const k=window.wp.notices,f=window.wp.escapeHtml,g={friendship:["friend","acquaintance","contact"],physical:["met"],professional:["co-worker","colleague"],geographical:["co-resident","neighbor"],family:["child","parent","sibling","spouse","kin"],romantic:["muse","crush","date","sweetheart"],identity:["me"]},v=["friendship","geographical","family"],w=Object.values(g).flat();function j({value:e=[],onChange:l}){return(0,u.jsx)(c.FormTokenField,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,__experimentalExpandOnFocus:!0,label:(0,r.__)("Relationship","blockroll"),value:e,suggestions:w,onChange:e=>l(function(e){return e.filter(e=>w.includes(e)).reduce((e,l)=>function(e,l){if("me"===l)return["me"];let o=e.filter(e=>"me"!==e);for(const e of v)g[e].includes(l)&&(o=o.filter(l=>!g[e].includes(l)));return[...o,l]}(e,l),[])}(e))})}function C(e,l,o){const t=[...e];return t.splice(o,0,...t.splice(l,1)),t}function S(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}const B={url:"",name:"",description:"",feedUrl:"",photo:"",xfn:[],added:""};function y({link:e,onSave:l,onCancel:o}){const[t,n]=(0,s.useState)({...B,...e}),[a,i]=(0,s.useState)(!1),[d,h]=(0,s.useState)(null),_=e=>l=>n(o=>({...o,[e]:l}));return(0,u.jsx)(c.Modal,{title:e?(0,r.__)("Edit link","blockroll"):(0,r.__)("Add link","blockroll"),size:"medium",onRequestClose:o,focusOnMount:"firstContentElement",children:(0,u.jsxs)("div",{className:"blockroll-form",children:[d&&(0,u.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,u.jsxs)("div",{className:"blockroll-form__row",children:[(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Address","blockroll"),placeholder:"https://example.com/",type:"url",value:t.url,onChange:_("url")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"secondary",isBusy:a,disabled:!t.url||a,onClick:()=>{i(!0),h(null),x()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}}).then(e=>n(l=>S(l,e))).catch(e=>h(e.message||(0,r.__)("The site could not be reached.","blockroll"))).finally(()=>i(!1))},children:(0,r.__)("Fetch details","blockroll")})]}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Name","blockroll"),value:t.name,onChange:_("name")}),(0,u.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Description","blockroll"),rows:2,value:t.description,onChange:_("description")}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Feed address","blockroll"),type:"url",value:t.feedUrl,onChange:_("feedUrl")}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Image address","blockroll"),type:"url",value:t.photo,onChange:_("photo")}),(0,u.jsx)(j,{value:t.xfn,onChange:_("xfn")}),(0,u.jsxs)("div",{className:"blockroll-form__footer",children:[(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:o,children:(0,r.__)("Cancel","blockroll")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",disabled:!t.url,onClick:()=>l({...t,added:t.added||(new Date).toISOString().slice(0,10)}),children:(0,r.__)("Save","blockroll")})]})]})})}var N=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function M({onImport:e,onClose:l}){const[o,t]=(0,s.useState)(""),[n,a]=(0,s.useState)(""),[i,d]=(0,s.useState)(""),[h,_]=(0,s.useState)(!1),[p,m]=(0,s.useState)(!1),[b,k]=(0,s.useState)(null),[f,g]=(0,s.useState)(null),v=e=>{e&&(e.text().then(t),a(e.name))},w={file:(0,u.jsxs)("div",{className:"blockroll-import-upload",children:[(0,u.jsx)(c.DropZone,{onFilesDrop:e=>v(e[0])}),(0,u.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:N,variant:"secondary",onChange:e=>v(e.target.files?.[0]),children:n||(0,r.__)("Choose or drop a file","blockroll")})]}),paste:(0,u.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),a("")}}),url:(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,u.jsx)(c.Modal,{title:(0,r.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,u.jsxs)("div",{className:"blockroll-form",children:[f&&(0,u.jsx)(c.Notice,{status:"error",isDismissible:!1,children:f}),(0,u.jsx)("p",{className:"blockroll-import-intro",children:(0,r.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,u.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,r.__)("Upload","blockroll")},{name:"paste",title:(0,r.__)("Paste","blockroll")},{name:"url",title:(0,r.__)("Address","blockroll")}],children:e=>(0,u.jsx)("div",{className:"blockroll-import-tab",children:w[e.name]})}),(0,u.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Fetch details for imported links","blockroll"),help:(0,r.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:_}),(0,u.jsxs)("div",{className:"blockroll-form__footer",children:[null!==b&&(0,u.jsx)("span",{children:(0,r.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ -(0,r.__)("%d links processed…","blockroll"),b)}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,r.__)("Cancel","blockroll")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:p,disabled:!o&&!i||p,onClick:async()=>{m(!0),g(null);try{const t=o?{opml:o}:{url:i},{links:n}=await x()({path:"/blockroll/v1/import",method:"POST",data:t}),r=h?await async function(e,l){const o=[];for(const t of e){try{const e=await x()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(S(t,e))}catch{o.push(t)}l(o.length)}return o}(n,k):n;e(r),l()}catch(e){g(e.message||(0,r.__)("The file could not be imported.","blockroll")),m(!1),k(null)}},children:(0,r.__)("Import","blockroll")})]})]})})}const O=window.wp.url;function D(e){return(0,O.cleanForSlug)(e)||"blogroll"}function T(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}(0,t.registerBlockType)(n.UU,{edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:t}){const{anchor:n,links:d,source:b,sortBy:g,perPage:v,showAvatars:w,showSort:j,showOpml:S,metadata:B}=e,N=B?.name||"",O=(0,a.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:H}=(0,a.useDispatch)(i.store),z=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=O.select(i.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,s.useEffect)(()=>{if(!n)return H(),void l({anchor:T(D(N),z())});if(t)return;const e=z();e.includes(n)&&(H(),l({anchor:T(n,e)}))},[n,t]);const A=(0,s.useRef)(N);(0,s.useEffect)(()=>{if(A.current===N)return;const e=!n||function(e,l){const o=D(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(n,A.current);A.current=N,e&&l({anchor:T(D(N),z())})},[N]);const P=(0,a.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:r,getSelectedBlockClientId:s}=e(i.store);if(!n||t)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&r(e)?.anchor===n)},[o,n,t]),{createWarningNotice:U,removeNotice:L}=(0,a.useDispatch)(k.store),F=(0,s.useRef)(null);(0,s.useEffect)(()=>{F.current&&(L(F.current),F.current=null),P&&(F.current=`blockroll-anchor-${n}`,U((0,r.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ -(0,r.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,f.escapeHTML)(n)),{id:F.current,isDismissible:!0,__unstableHTML:!0}))},[P,n]),(0,s.useEffect)(()=>()=>{F.current&&L(F.current)},[]);const[I,E]=(0,s.useState)(null),[R,V]=(0,s.useState)(!1),[G,Z]=(0,s.useState)([{label:(0,r.__)("Manual links","blockroll"),value:"manual"}]),[$,W]=(0,s.useState)([]),[q,J]=(0,s.useState)(!1),[X,K]=(0,s.useState)(null),[Q,Y]=(0,s.useState)(!1),ee="manual",le=G.some(e=>b===e.value),oe=!b||Q&&!le?ee:b,te=JSON.stringify(e),ne=G.filter(e=>ee!==e.value),re=G.find(e=>oe===e.value)||G[0];(0,s.useEffect)(()=>{x()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(Z(e),Y(!0))}).catch(()=>{})},[]),(0,s.useEffect)(()=>{let e=!0;return ee===oe?(W([]),K(null),J(!1),()=>{e=!1}):(J(!0),K(null),x()({path:`/blockroll/v1/sources/${oe}/links?attributes=`+encodeURIComponent(te)}).then(l=>{e&&W(Array.isArray(l)?l:[])}).catch(l=>{e&&(W([]),K(l.message||(0,r.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&J(!1)}),()=>{e=!1})},[oe,te]);const se=(0,u.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,u.jsx)(c.Button,{variant:"primary",onClick:()=>E("new"),children:(0,r.__)("Add link","blockroll")}),(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>V(!0),children:(0,r.__)("Import links","blockroll")}),!d.length&&ee===oe&&ne.map(e=>(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,r.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ -(0,r.__)("Use %s","blockroll"),e.label)},e.value))]});let ae=null;ee!==oe||d.length||(ae=(0,u.jsx)(c.Placeholder,{icon:"admin-links",label:(0,r.__)("Blogroll","blockroll"),instructions:(0,r.__)("Share a list of the blogs and sites you follow.","blockroll"),children:se}));const ie=(0,u.jsx)("div",{className:"blockroll-editor-actions",children:(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:ee}),children:(0,r.__)("Use manual links","blockroll")})}),ce=(e,o=!1)=>(0,u.jsx)("ul",{className:"blockroll-editor-list",children:e.map((e,t)=>(0,u.jsxs)("li",{children:[w&&(e.photo?(0,u.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,u.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,u.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,u.jsx)("strong",{children:e.name||e.url}),(0,u.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]}),!o&&(0,u.jsxs)("span",{className:"blockroll-editor-list__actions",children:[(0,u.jsx)(c.Button,{size:"compact",icon:h,label:(0,r.__)("Move up","blockroll"),disabled:0===t,onClick:()=>l({links:C(d,t,t-1)})}),(0,u.jsx)(c.Button,{size:"compact",icon:_,label:(0,r.__)("Move down","blockroll"),disabled:t===d.length-1,onClick:()=>l({links:C(d,t,t+1)})}),(0,u.jsx)(c.Button,{size:"compact",icon:p,label:(0,r.__)("Edit","blockroll"),onClick:()=>E(t)}),(0,u.jsx)(c.Button,{size:"compact",icon:m,label:(0,r.__)("Remove","blockroll"),isDestructive:!0,onClick:()=>l({links:d.filter((e,l)=>l!==t)})})]})]},e.url))});return(0,u.jsxs)("div",{...(0,i.useBlockProps)(),children:[(0,u.jsx)(i.InspectorControls,{children:(0,u.jsxs)(c.PanelBody,{title:(0,r.__)("Blogroll settings","blockroll"),children:[(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Name","blockroll"),help:(0,s.createInterpolateElement)((0,r.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ -(0,r.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),n||D(N)),{code:(0,u.jsx)("code",{})}),placeholder:(0,r.__)("Blogroll","blockroll"),value:N,onChange:e=>{const o={...B};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,u.jsx)(c.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Sort by","blockroll"),value:g,options:[{label:(0,r.__)("Name","blockroll"),value:"name"},{label:(0,r.__)("Newest first","blockroll"),value:"added"},{label:(0,r.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,u.jsx)(c.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Links per page","blockroll"),help:(0,r.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:v,onChange:e=>l({perPage:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Show images","blockroll"),checked:w,onChange:e=>l({showAvatars:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Let visitors sort the list","blockroll"),checked:j,onChange:e=>l({showSort:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Show download link","blockroll"),help:(0,r.__)("Links to the list as an OPML file.","blockroll"),checked:S,onChange:e=>l({showOpml:e})})]})}),null!==I&&(0,u.jsx)(y,{link:"new"===I?void 0:d[I],onSave:e=>{const o=[...d];"new"===I?o.push(e):o[I]=e,l({links:o,source:ee}),E(null)},onCancel:()=>E(null)}),R&&(0,u.jsx)(M,{onImport:e=>{const o=new Set(d.map(e=>e.url));l({source:ee,links:[...d,...e.filter(e=>!o.has(e.url))]})},onClose:()=>V(!1)}),ee!==oe?(0,u.jsxs)("div",{className:"blockroll-editor",children:[(0,u.jsx)(c.Placeholder,{icon:"admin-links",label:re.label,instructions:q?(0,r.__)("Loading source preview…","blockroll"):(0,r.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ -(0,r.__)("%d links will be shown when the block is rendered.","blockroll"),$.length),children:ie}),re.help&&(0,u.jsxs)("p",{children:[re.help,re.helpUrl&&(0,u.jsxs)(u.Fragment,{children:[" ",(0,u.jsx)("a",{href:re.helpUrl,children:(0,r.__)("Manage source","blockroll")})]})]}),X&&(0,u.jsx)("p",{children:X}),!X&&!q&&!!$.length&&ce($,!0)]}):ae||(0,u.jsxs)("div",{className:"blockroll-editor",children:[ce(d),se]})]})},save:()=>null})}};const l={};function o(t){const n=l[t];if(void 0!==n)return n.exports;const r=l[t]={exports:{}};return e[t](r,r.exports,o),r.exports}o.m=e,(()=>{const e=[];o.O=(l,t,n,r)=>{if(t){r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];return void(e[s]=[t,n,r])}let a=1/0;for(s=0;s=r)&&Object.keys(o.O).every(e=>o.O[e](t[i]))?t.splice(i--,1):(c=!1,r{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[n,r,s]=t;var a,i,c=0;if(n.some(l=>0!==e[l])){for(a in r)o.o(r,a)&&(o.m[a]=r[a]);if(s)var d=s(o)}for(l&&l(t);co(408));t=o.O(t)})(); \ No newline at end of file +(()=>{"use strict";var e={669(e,l,t){const o=window.wp.blocks,n=window.wp.blockEditor,r=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.compose,u=window.wp.components,d=window.wp.apiFetch;var h=t.n(d);const p=window.wp.notices,m=window.wp.escapeHtml,b=JSON.parse('{"UU":"blockroll/link"}').UU;function _(e){return(e||[]).some(e=>b===e.name)}function f(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function k(e){return String(e||"").trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//,"").replace(/^www\./,"").replace(/\/+$/,"")}function g(e,l){return h()({path:"/blockroll/v1/discover",method:"POST",data:{url:e},signal:l})}function x(e){return"AbortError"===e?.name}function w(){const e=(0,a.useRef)();return(0,a.useEffect)(()=>(e.current=new AbortController,()=>e.current.abort()),[]),e}const v=window.ReactJSXRuntime;function j({value:e,onChange:l,onSubmit:t,isBusy:o,message:n,buttonLabel:r}){return(0,v.jsxs)("form",{className:"blockroll-form__row",onSubmit:e=>{e.preventDefault(),o||t()},children:[(0,v.jsx)(u.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,help:n,className:n?"has-message":void 0,placeholder:"example.com",type:"text",inputMode:"url",autoComplete:"off",spellCheck:!1,value:e,readOnly:o,onChange:l}),(0,v.jsx)(u.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:o,"aria-disabled":o,disabled:!e.trim(),children:r||(0,s.__)("Add","blockroll")})]})}const y=()=>{};function S({anchor:e,onAdd:l,isKnown:t,onClose:o}){const[n,r]=(0,a.useState)(""),[i,c]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),[p,m]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),k=w();let S=d;return p?S=(0,s.__)("This site is in the list already.","blockroll"):b&&(S=(0,s.__)("Only web addresses can be added.","blockroll")),(0,v.jsx)(u.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,onFocusOutside:y,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,v.jsx)(j,{value:n,onChange:e=>{r(e),h(null),m(!1),_(!1)},onSubmit:()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)&&!/^[^:/?#]+:\d/.test(l)?l:`https://${l}`}(n);!function(e){try{const{protocol:l,hostname:t}=new URL(String(e||"").trim());return/^https?:$/i.test(l)&&""!==t}catch{return!1}}(e)?_(!0):t(e)?m(!0):d?l({url:e}):(c(!0),function(e,l){return g(e,l).then(l=>f({url:e},l))}(e,k.current.signal).then(l,e=>{x(e)||(h(e.message||(0,s.__)("The site could not be reached.","blockroll")),c(!1))}))},isBusy:i,message:S,buttonLabel:d?(0,s.__)("Add anyway","blockroll"):void 0})})}function C(e){return(0,o.createBlock)(b,e)}const B=window.wp.primitives;var N=(0,v.jsx)(B.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,v.jsx)(B.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function O({onImport:e,onClose:l}){const[t,o]=(0,a.useState)(""),[n,r]=(0,a.useState)(""),[i,c]=(0,a.useState)(""),[d,p]=(0,a.useState)(!1),[m,b]=(0,a.useState)(!1),[_,k]=(0,a.useState)(null),[j,y]=(0,a.useState)(null),S=w(),C=e=>{e&&(e.text().then(o),r(e.name))},B={file:(0,v.jsxs)("div",{className:"blockroll-import-upload",children:[(0,v.jsx)(u.DropZone,{onFilesDrop:e=>C(e[0])}),(0,v.jsx)(u.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:N,variant:"secondary",onChange:e=>C(e.target.files?.[0]),children:n||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,v.jsx)(u.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:t,onChange:e=>{o(e),r("")}}),url:(0,v.jsx)(u.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:c})};return(0,v.jsx)(u.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,v.jsxs)("div",{className:"blockroll-form",children:[j&&(0,v.jsx)(u.Notice,{status:"error",isDismissible:!1,children:j}),(0,v.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,v.jsx)(u.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,v.jsx)("div",{className:"blockroll-import-tab",children:B[e.name]})}),(0,v.jsx)(u.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:d,onChange:p}),(0,v.jsxs)("div",{className:"blockroll-form__footer",children:[null!==_&&(0,v.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ +(0,s.__)("%d links processed…","blockroll"),_)}),(0,v.jsx)(u.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,v.jsx)(u.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:m,disabled:!t&&!i||m,onClick:async()=>{b(!0),y(null);try{const o=t?{opml:t}:{url:i},{links:n}=await h()({path:"/blockroll/v1/import",method:"POST",data:o,signal:S.current.signal}),r=d?await async function(e,l,t){const o=[];for(const n of e){try{o.push(f(n,await g(n.url,t)))}catch(e){if(x(e))throw e;o.push(n)}l(o.length)}return o}(n,k,S.current.signal):n;e(r),l()}catch(e){if(x(e))return;y(e.message||(0,s.__)("The file could not be imported.","blockroll")),b(!1),k(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const A=window.wp.url;function M(e){return(0,A.cleanForSlug)(e)||"blogroll"}function H(e,l){let t=e,o=0;for(;l.includes(t);)o+=1,t=`${e}-${o}`;return t}const T=(e,l)=>e.getBlockOrder(l).filter(l=>b===e.getBlockName(l)),P=[{attributes:r.uK,supports:r.xY,save:()=>null,isEligible:function(e,l){return function(e){return Array.isArray(e?.links)&&e.links.length>0}(e)&&!_(l)},migrate:(e,l)=>{const[t,o]=function(e){return[{...e,links:[]},(e.links||[]).filter(e=>e?.url)]}(e),n=l||[];return[t,_(n)?n:[...n,...o.map(C)]]}}];var D=(0,v.jsx)(B.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,v.jsx)(B.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,o.registerBlockType)(r.UU,{icon:D,edit:function({attributes:e,setAttributes:l,clientId:t,isSelected:o}){const{anchor:r,source:d,sortBy:_,perPage:f,showAvatars:g,showSort:x,showOpml:w,metadata:j}=e,y=j?.name||"",B=(0,i.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:N}=(0,i.useDispatch)(n.store),A=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=B.select(n.store);return e().filter(e=>e!==t).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!r)return N(),void l({anchor:H(M(y),A())});if(o)return;const e=A();e.includes(r)&&(N(),l({anchor:H(r,e)}))},[r,o]);const P=(0,a.useRef)(y);(0,a.useEffect)(()=>{if(P.current===y)return;const e=!r||function(e,l){const t=M(l);return e===t||e.startsWith(t+"-")&&/^\d+$/.test(e.slice(t.length+1))}(r,P.current);P.current=y,e&&l({anchor:H(M(y),A())})},[y]);const D=(0,i.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:s,getSelectedBlockClientId:a}=e(n.store);if(!r||o)return!1;const i=a();return l().some(e=>e!==t&&e!==i&&s(e)?.anchor===r)},[t,r,o]),{createWarningNotice:L,removeNotice:U}=(0,i.useDispatch)(p.store),z=(0,a.useRef)(null);(0,a.useEffect)(()=>{z.current&&(U(z.current),z.current=null),D&&(z.current=`blockroll-anchor-${r}`,L((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ +(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,m.escapeHTML)(r)),{id:z.current,isDismissible:!0,__unstableHTML:!0}))},[D,r]),(0,a.useEffect)(()=>()=>{z.current&&U(z.current)},[]);const[I,E]=(0,a.useState)(!1),[F,V]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[R,$]=(0,a.useState)([]),[Z,J]=(0,a.useState)(!1),[K,W]=(0,a.useState)(null),[G,Y]=(0,a.useState)(!1),q="manual",X=F.some(e=>d===e.value),Q=!d||G&&!X?q:d,ee=JSON.stringify(e),le=F.filter(e=>q!==e.value),te=F.find(e=>Q===e.value)||F[0];(0,a.useEffect)(()=>{h()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(V(e),Y(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return q===Q?($([]),W(null),J(!1),()=>{e=!1}):(J(!0),W(null),h()({path:`/blockroll/v1/sources/${Q}/links?attributes=`+encodeURIComponent(ee)}).then(l=>{e&&$(Array.isArray(l)?l:[])}).catch(l=>{e&&($([]),W(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&J(!1)}),()=>{e=!1})},[Q,ee]);const oe=(0,i.useSelect)(e=>e(n.store).getBlockCount(t),[t]),ne=(0,i.useSelect)(e=>T(e(n.store),t).length,[t]),{insertBlock:re,insertBlocks:se}=(0,i.useDispatch)(n.store),[ae,ie]=(0,a.useState)(!1),[ce,ue]=(0,a.useState)(),de=(0,c.__experimentalUseFocusOutside)(()=>ie(!1)),he=()=>{const e=B.select(n.store);return new Set(T(e,t).map(l=>k(e.getBlockAttributes(l)?.url)))};(0,a.useEffect)(()=>{G&&!X&&q!==d&&ne&&(N(),l({source:q}))},[G,X,d,ne]);const pe=(0,v.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,v.jsxs)("span",{...de,children:[(0,v.jsx)(u.Button,{variant:"primary",ref:ue,"aria-expanded":ae,onClick:()=>ie(!ae),children:(0,s.__)("Add link","blockroll")}),ae&&(0,v.jsx)(S,{anchor:ce,onAdd:e=>{re(C({...e,added:(new Date).toISOString().slice(0,10)}),void 0,t),ie(!1)},isKnown:e=>he().has(k(e)),onClose:()=>ie(!1)})]}),(0,v.jsx)(u.Button,{variant:"secondary",onClick:()=>E(!0),children:(0,s.__)("Import links","blockroll")}),!ne&&le.map(e=>(0,v.jsx)(u.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ +(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let me=null;q!==Q||oe||(me=(0,v.jsx)(u.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:pe}));const be=(0,n.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:[b],templateLock:!1,renderAppender:!1,placeholder:me}),_e=(0,v.jsx)("div",{className:"blockroll-editor-actions",children:(0,v.jsx)(u.Button,{variant:"secondary",onClick:()=>l({source:q}),children:(0,s.__)("Use manual links","blockroll")})}),fe=(0,n.useBlockProps)();return(0,v.jsxs)("div",{...fe,children:[(0,v.jsx)(n.InspectorControls,{children:(0,v.jsxs)(u.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,v.jsx)(u.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ +(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),r||M(y)),{code:(0,v.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:y,onChange:e=>{const t={...j};e?t.name=e:delete t.name,l({metadata:Object.keys(t).length?t:void 0})}}),(0,v.jsx)(u.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:_,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,v.jsx)(u.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:f,onChange:e=>l({perPage:e})}),(0,v.jsx)(u.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:g,onChange:e=>l({showAvatars:e})}),(0,v.jsx)(u.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:x,onChange:e=>l({showSort:e})}),(0,v.jsx)(u.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:w,onChange:e=>l({showOpml:e})})]})}),I&&(0,v.jsx)(O,{onImport:e=>{const l=he(),o=e.filter(e=>{const t=k(e.url);return!(!t||l.has(t)||(l.add(t),0))}).map(C);o.length&&se(o,void 0,t)},onClose:()=>E(!1)}),q!==Q?(0,v.jsxs)("div",{className:"blockroll-editor",children:[(0,v.jsx)(u.Placeholder,{icon:"admin-links",label:te.label,instructions:Z?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ +(0,s.__)("%d links will be shown when the block is rendered.","blockroll"),R.length),children:_e}),te.help&&(0,v.jsxs)("p",{children:[te.help,te.helpUrl&&(0,v.jsxs)(v.Fragment,{children:[" ",(0,v.jsx)("a",{href:te.helpUrl,children:(0,s.__)("Manage source","blockroll")})]})]}),K&&(0,v.jsx)("p",{children:K}),!K&&!Z&&!!R.length&&(ke=R,(0,v.jsx)("ul",{className:"blockroll-editor-list",children:ke.map(e=>(0,v.jsxs)("li",{children:[g&&(e.photo?(0,v.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,v.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,v.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,v.jsx)("strong",{children:e.name||e.url}),(0,v.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]})]},e.url))}))]}):(0,v.jsxs)("div",{className:"blockroll-editor",children:[(0,v.jsx)("div",{...be}),!me&&pe]})]});var ke},save:()=>(0,v.jsx)(n.InnerBlocks.Content,{}),deprecated:P})}};const l={};function t(o){const n=l[o];if(void 0!==n)return n.exports;const r=l[o]={exports:{}};return e[o](r,r.exports,t),r.exports}t.m=e,(()=>{const e=[];t.O=(l,o,n,r)=>{if(o){r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];return void(e[s]=[o,n,r])}let a=1/0;for(s=0;s=r)&&Object.keys(t.O).every(e=>t.O[e](o[i]))?o.splice(i--,1):(c=!1,r{const l=e&&e.__esModule?()=>e.default:()=>e;return t.d(l,{a:l}),l},t.d=(e,l)=>{if(Array.isArray(l))for(var o=0;oObject.hasOwn(e,l),(()=>{const e={39:0,375:0};t.O.j=l=>0===e[l];const l=(l,o)=>{let[n,r,s]=o;var a,i,c=0;if(n.some(l=>0!==e[l])){for(a in r)t.o(r,a)&&(t.m[a]=r[a]);if(s)var u=s(t)}for(l&&l(o);ct(669));o=t.O(o)})(); \ No newline at end of file diff --git a/build/blogroll/render.php b/build/blogroll/render.php index 06786a5..0d269ef 100644 --- a/build/blogroll/render.php +++ b/build/blogroll/render.php @@ -7,7 +7,8 @@ * * @package Blockroll * - * @var array $attributes Block attributes. + * @var array $attributes Block attributes. + * @var WP_Block $block The block; its link blocks are its inner blocks. */ use Blockroll\Links; @@ -16,7 +17,7 @@ defined( 'ABSPATH' ) || exit; -$blockroll_links = Sources::links( $attributes ); +$blockroll_links = Sources::links( $attributes, $block->parsed_block['innerBlocks'] ?? array() ); $blockroll_sortable = $attributes['showSort']; diff --git a/build/link/block.json b/build/link/block.json new file mode 100644 index 0000000..c5127b1 --- /dev/null +++ b/build/link/block.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "blockroll/link", + "title": "Link", + "category": "widgets", + "description": "One site in a blogroll.", + "textdomain": "blockroll", + "parent": [ + "blockroll/blogroll" + ], + "supports": { + "html": false, + "inserter": false, + "reusable": false + }, + "usesContext": [ + "blockroll/showAvatars" + ], + "attributes": { + "url": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + }, + "description": { + "type": "string", + "default": "" + }, + "feedUrl": { + "type": "string", + "default": "" + }, + "photo": { + "type": "string", + "default": "" + }, + "xfn": { + "type": "array", + "default": [] + }, + "added": { + "type": "string", + "default": "" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css" +} \ No newline at end of file diff --git a/build/link/index-rtl.css b/build/link/index-rtl.css new file mode 100644 index 0000000..7aefcc7 --- /dev/null +++ b/build/link/index-rtl.css @@ -0,0 +1 @@ +.wp-block-blockroll-link .u-url{display:block}.wp-block-blockroll-link .blockroll-link__name,.wp-block-blockroll-link .blockroll-meta>span{display:contents}.wp-block-blockroll-link.h-card .blockroll-link__photo{align-self:start;border-radius:50%;grid-row:1/span 2;height:auto;margin-left:.75em;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__photo .blockroll-no-photo,.wp-block-blockroll-link.h-card .blockroll-link__photo .u-photo{margin-left:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button{align-items:center;color:inherit;display:inline-flex;font:inherit;gap:.25em;height:auto;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button:hover{text-decoration:underline}.blockroll-link__meta-overlay .components-popover__content{min-width:300px;padding:16px} diff --git a/build/link/index.asset.php b/build/link/index.asset.php new file mode 100644 index 0000000..597d548 --- /dev/null +++ b/build/link/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '04539ce9911617c2f240'); diff --git a/build/link/index.css b/build/link/index.css new file mode 100644 index 0000000..94bcf1c --- /dev/null +++ b/build/link/index.css @@ -0,0 +1 @@ +.wp-block-blockroll-link .u-url{display:block}.wp-block-blockroll-link .blockroll-link__name,.wp-block-blockroll-link .blockroll-meta>span{display:contents}.wp-block-blockroll-link.h-card .blockroll-link__photo{align-self:start;border-radius:50%;grid-row:1/span 2;height:auto;margin-right:.75em;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__photo .blockroll-no-photo,.wp-block-blockroll-link.h-card .blockroll-link__photo .u-photo{margin-right:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button{align-items:center;color:inherit;display:inline-flex;font:inherit;gap:.25em;height:auto;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button:hover{text-decoration:underline}.blockroll-link__meta-overlay .components-popover__content{min-width:300px;padding:16px} diff --git a/build/link/index.js b/build/link/index.js new file mode 100644 index 0000000..c98a2ff --- /dev/null +++ b/build/link/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const e={n:o=>{const n=o&&o.__esModule?()=>o.default:()=>o;return e.d(n,{a:n}),n},d:(o,n)=>{if(Array.isArray(n))for(var t=0;tObject.hasOwn(e,o)},o=window.wp.blocks,n=JSON.parse('{"UU":"blockroll/link"}'),t=window.wp.i18n,l=window.wp.element,r=window.wp.compose,s=window.wp.blockEditor,a=window.wp.components,i=window.wp.primitives,c=window.ReactJSXRuntime;var u=(0,c.jsx)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,c.jsx)(i.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),d=(0,c.jsx)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,c.jsx)(i.Path,{d:"M5 10.2h-.8v1.5H5c1.9 0 3.8.8 5.1 2.1 1.4 1.4 2.1 3.2 2.1 5.1v.8h1.5V19c0-2.3-.9-4.5-2.6-6.2-1.6-1.6-3.8-2.6-6.1-2.6zm10.4-1.6C12.6 5.8 8.9 4.2 5 4.2h-.8v1.5H5c3.5 0 6.9 1.4 9.4 3.9s3.9 5.8 3.9 9.4v.8h1.5V19c0-3.9-1.6-7.6-4.4-10.4zM4 20h3v-3H4v3z"})});function h({value:e,onChange:o,onSubmit:n,isBusy:l,message:r,buttonLabel:s}){return(0,c.jsxs)("form",{className:"blockroll-form__row",onSubmit:e=>{e.preventDefault(),l||n()},children:[(0,c.jsx)(a.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,t.__)("Address","blockroll"),hideLabelFromVision:!0,help:r,className:r?"has-message":void 0,placeholder:"example.com",type:"text",inputMode:"url",autoComplete:"off",spellCheck:!1,value:e,readOnly:l,onChange:o}),(0,c.jsx)(a.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:l,"aria-disabled":l,disabled:!e.trim(),children:s||(0,t.__)("Add","blockroll")})]})}const m={friendship:["friend","acquaintance","contact"],physical:["met"],professional:["co-worker","colleague"],geographical:["co-resident","neighbor"],family:["child","parent","sibling","spouse","kin"],romantic:["muse","crush","date","sweetheart"],identity:["me"]},p=["friendship","geographical","family"],f=Object.values(m).flat();function x({value:e=[],onChange:o}){return(0,c.jsx)(a.FormTokenField,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,__experimentalExpandOnFocus:!0,label:(0,t.__)("Relationship","blockroll"),value:e,suggestions:f,onChange:e=>o(function(e){return e.filter(e=>f.includes(e)).reduce((e,o)=>function(e,o){if("me"===o)return["me"];let n=e.filter(e=>"me"!==e);for(const e of p)m[e].includes(o)&&(n=n.filter(o=>!m[e].includes(o)));return[...n,o]}(e,o),[])}(e))})}const b=window.wp.apiFetch;var g=e.n(b);function _(){return(new Date).toISOString().slice(0,10)}function w(e){try{const{protocol:o,hostname:n}=new URL(String(e||"").trim());return/^https?:$/i.test(o)&&""!==n}catch{return!1}}n.UU;const v=()=>{};function k({isOpen:e,onToggle:o,onClose:n,className:t,focusOnMount:s="firstElement",label:i,children:u}){const[d,h]=(0,l.useState)(),m=(0,r.__experimentalUseFocusOutside)(n);return(0,c.jsxs)("span",{...m,children:[(0,c.jsx)(a.Button,{className:t,ref:h,"aria-expanded":e,onClick:o,children:i}),e&&(0,c.jsx)(a.Popover,{anchor:d,placement:"bottom-start",shift:!0,onClose:n,onFocusOutside:v,focusOnMount:s,className:"blockroll-link__meta-overlay",children:u})]})}const j=()=>{};var C=(0,c.jsx)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,c.jsx)(i.Path,{d:"M12 11v1.5h8V11h-8zm-6-1c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"})});(0,o.registerBlockType)(n.UU,{icon:C,edit:function({attributes:e,setAttributes:o,context:n,isSelected:i}){const{url:m,name:p,description:f,photo:b,feedUrl:v,xfn:C,added:S}=e,[y,O]=(0,l.useState)(""),[N,B]=(0,l.useState)(!1),[F,T]=(0,l.useState)(null),U=(0,l.useRef)(S);U.current=S;const M=function(){const e=(0,l.useRef)();return(0,l.useEffect)(()=>(e.current=new AbortController,()=>e.current.abort()),[]),e}(),[A,R]=(0,l.useState)(null),H=()=>R(null),z=(0,r.__experimentalUseFocusOutside)(H),[P,L]=(0,l.useState)(),[D,V]=(0,l.useState)(null),E=()=>V(null),I=e=>V(e===D?null:e),q=(0,l.useRef)({}),G=n["blockroll/showAvatars"]??!0,Z=i||!!v,J=i||C.length>0,$=(0,s.useBlockProps)({className:m?"h-card":void 0}),X=b?(0,c.jsx)("img",{className:"u-photo",src:b,alt:""}):(0,c.jsx)("span",{className:"blockroll-no-photo"}),K=(0,c.jsx)(s.InspectorControls,{children:(0,c.jsx)(a.PanelBody,{title:(0,t.__)("Link settings","blockroll"),children:(0,c.jsx)(a.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,t.__)("Added","blockroll"),help:(0,t.__)("Used when the list is sorted newest first.","blockroll"),type:"date",value:S,onChange:e=>o({added:e})})})});if(!m){const e=()=>{const e=function(e){const o=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(o)&&!/^[^:/?#]+:\d/.test(o)?o:`https://${o}`}(y);w(e)?(B(!0),function(e,o){return function(e,o){return g()({path:"/blockroll/v1/discover",method:"POST",data:{url:e},signal:o})}(e,o).then(o=>function(e,o){return{...e,name:e.name||o.name,description:e.description||o.description,feedUrl:e.feedUrl||o.feedUrl,photo:e.photo||o.photo}}({url:e},o))}(e,M.current.signal).then(e=>{o({...e,added:U.current||_()}),B(!1)},n=>{(function(e){return"AbortError"===e?.name})(n)||(o({url:e,added:U.current||_()}),B(!1))})):T((0,t.__)("Only web addresses can be added.","blockroll"))};return(0,c.jsxs)("div",{...$,children:[K,(0,c.jsx)(h,{value:y,message:F,onChange:e=>{O(e),T(null)},onSubmit:e,isBusy:N})]})}return(0,c.jsxs)(c.Fragment,{children:[K,(0,c.jsxs)("div",{...$,children:[G&&(0,c.jsx)(s.MediaReplaceFlow,{mediaURL:b,allowedTypes:["image"],accept:"image/*",onSelect:e=>o({photo:e.url}),onSelectURL:e=>o({photo:e}),onReset:()=>o({photo:""}),popoverProps:{onFocusOutside:e=>{const o=e.target.ownerDocument.activeElement;!o?.closest('[role="dialog"]')&&q.current.isOpen&&q.current.onToggle()}},renderToggle:({children:e,...o})=>(q.current={isOpen:o["aria-expanded"],onToggle:o.onClick},(0,c.jsx)(a.Button,{...o,className:"blockroll-link__photo",label:(0,t.__)("Image","blockroll"),children:X}))}),(0,c.jsxs)("span",{className:"blockroll-link__name",...z,children:[(0,c.jsx)(s.BlockControls,{group:"block",children:(0,c.jsx)(a.ToolbarButton,{icon:u,title:(0,t.__)("Link","blockroll"),isActive:!!A,onClick:()=>R("toolbar"===A?null:"toolbar")})}),i&&A&&(0,c.jsx)(a.Popover,{anchor:P,placement:"bottom-start",shift:!0,onClose:H,onFocusOutside:j,focusOnMount:"toolbar"===A&&"firstElement",children:(0,c.jsx)(s.LinkControl,{value:{url:m,title:p},settings:[],hasTextControl:!0,forceIsEditingLink:!0,showInitialSuggestions:!1,onChange:e=>{const n=e.url||"";o({url:!n||w(n)?n:m,name:e.title??p}),H()},onCancel:H})},A),(0,c.jsx)(s.RichText,{ref:L,onFocus:()=>R("name"),disableLineBreaks:!0,tagName:"a",className:"u-url p-name",href:w(m)?m:void 0,value:p,allowedFormats:[],withoutInteractiveFormatting:!0,placeholder:(0,t.__)("Site name","blockroll"),onChange:e=>o({name:e})})]}),(i||f)&&(0,c.jsx)(s.RichText,{disableLineBreaks:!0,tagName:"p",className:"p-note",value:f,allowedFormats:[],withoutInteractiveFormatting:!0,placeholder:(0,t.__)("Description","blockroll"),onChange:e=>o({description:e})}),(Z||J)&&(0,c.jsxs)("div",{className:"blockroll-meta",children:[Z&&(0,c.jsx)(k,{className:"blockroll-feed blockroll-link__meta-button",isOpen:"feed"===D,onToggle:()=>I("feed"),onClose:E,label:(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(a.Icon,{icon:d,className:"blockroll-feed-icon"}),v?(0,t.__)("feed","blockroll"):(0,t.__)("Add feed","blockroll")]}),children:(0,c.jsx)(a.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,t.__)("Feed address","blockroll"),help:(0,t.__)("The RSS or Atom feed of the site, for readers who subscribe to your list.","blockroll"),type:"url",value:v,onChange:e=>o({feedUrl:e})})}),Z&&J&&(0,c.jsx)("span",{className:"blockroll-divider","aria-hidden":"true",children:"·"}),J&&(0,c.jsx)(k,{className:"blockroll-link__meta-button",isOpen:"xfn"===D,onToggle:()=>I("xfn"),onClose:E,focusOnMount:!0,label:C.length>0?(0,c.jsx)("span",{className:"blockroll-xfn",children:C.map(e=>(0,c.jsx)("span",{children:e},e))}):(0,t.__)("Add relationship","blockroll"),children:(0,c.jsx)("div",{ref:e=>e&&e.ownerDocument.defaultView.requestAnimationFrame(()=>e.querySelector("input")?.focus()),children:(0,c.jsx)(x,{value:C,onChange:e=>o({xfn:e})})})})]})]})]})},save:()=>null})})(); \ No newline at end of file diff --git a/docs/developers.md b/docs/developers.md index 3c61413..ac096e6 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -4,9 +4,15 @@ Notes for people who build on the plugin or work on it. Everything a user needs ## How it works -The links live in the block attributes, so the whole blogroll travels with the post -content. Each link holds `url`, `name`, `description`, `feedUrl`, `photo`, an `xfn` -array, and the date it was added. +Every link is a block of its own, `blockroll/link`, inside the `blockroll/blogroll` +block, so the whole blogroll travels with the post content. A link block holds `url`, +`name`, `description`, `feedUrl`, `photo`, an `xfn` array, and the date it was `added`. +It has no output of its own: the blogroll renders the whole list on the server, because +sorting and paging reorder and cut it. Blogrolls saved before 2.0 kept their links in a +`links` attribute of the blogroll block. The server still reads that attribute when a +block has no link blocks, so such a post renders without being opened; the editor +migrates it to link blocks when it is opened (a block deprecation, see +`src/blogroll/deprecated.js`). Every entry is marked up as an [h-card](https://microformats.org/wiki/h-card) with [XFN](https://gmpg.org/xfn/) relationships on the link, in an diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..be4dfe5 --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,18 @@ +/** + * The config of @wordpress/scripts, with one addition: the focus-outside + * hook of @wordpress/compose. It has kept its experimental prefix for + * years, and it is what the editor's own popovers and dropdowns use. + */ +const config = require( '@wordpress/scripts/config/eslint.config.cjs' ); + +module.exports = [ + ...config, + { + rules: { + '@wordpress/no-unsafe-wp-apis': [ + 'error', + { '@wordpress/compose': [ '__experimentalUseFocusOutside' ] }, + ], + }, + }, +]; diff --git a/includes/class-opml.php b/includes/class-opml.php index caf5b68..9af9eb3 100644 --- a/includes/class-opml.php +++ b/includes/class-opml.php @@ -181,7 +181,7 @@ public static function extract_groups( $post ) { $walker = function ( $blocks ) use ( &$walker, &$groups ) { foreach ( $blocks as $block ) { if ( 'blockroll/blogroll' === $block['blockName'] ) { - $links = Sources::links( (array) ( $block['attrs'] ?? array() ) ); + $links = Sources::links( (array) ( $block['attrs'] ?? array() ), $block['innerBlocks'] ?? array() ); if ( $links ) { $groups[] = array( 'name' => \sanitize_text_field( (string) ( $block['attrs']['metadata']['name'] ?? '' ) ), diff --git a/includes/class-sources.php b/includes/class-sources.php index d25cbf6..7e45fba 100644 --- a/includes/class-sources.php +++ b/includes/class-sources.php @@ -16,6 +16,11 @@ class Sources { */ const MANUAL = 'manual'; + /** + * The block that holds one link of a manual blogroll. + */ + const LINK_BLOCK = 'blockroll/link'; + /** * Get available source labels. * @@ -103,13 +108,18 @@ public static function help_url( $source ) { /** * Resolve and normalize the links for a block's selected source. * - * @param array $attributes Block attributes. + * @param array $attributes Block attributes. + * @param array $inner_blocks Parsed inner blocks of the block. * @return array Normalized links. */ - public static function links( $attributes ) { + public static function links( $attributes, $inner_blocks = array() ) { $source = self::source( $attributes ); if ( self::MANUAL === $source ) { - $links = (array) ( $attributes['links'] ?? array() ); + $links = self::link_blocks( $inner_blocks ); + if ( ! $links ) { + // Blocks saved before links became blocks of their own. + $links = (array) ( $attributes['links'] ?? array() ); + } } else { /** * Provide links for a selected blogroll source. @@ -138,6 +148,24 @@ function ( $link ) { ); } + /** + * Collect the attributes of the link blocks among a block's inner blocks. + * + * Other blocks are ignored. + * + * @param array $inner_blocks Parsed inner blocks. + * @return array Raw link arrays, in block order. + */ + private static function link_blocks( $inner_blocks ) { + $links = array(); + foreach ( (array) $inner_blocks as $block ) { + if ( self::LINK_BLOCK === ( $block['blockName'] ?? '' ) ) { + $links[] = (array) ( $block['attrs'] ?? array() ); + } + } + return $links; + } + /** * Get a block's selected source. * diff --git a/package-lock.json b/package-lock.json index 347a27a..5e48ffd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "@wordpress/block-editor": "^16.1.0", "@wordpress/blocks": "^15.25.0", "@wordpress/components": "^38.0.0", + "@wordpress/compose": "^8.8.0", "@wordpress/data": "^10.55.0", "@wordpress/element": "^8.4.0", "@wordpress/env": "^11.14.0", diff --git a/package.json b/package.json index 6481ddc..41d2606 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@wordpress/block-editor": "^16.1.0", "@wordpress/blocks": "^15.25.0", "@wordpress/components": "^38.0.0", + "@wordpress/compose": "^8.8.0", "@wordpress/data": "^10.55.0", "@wordpress/element": "^8.4.0", "@wordpress/env": "^11.14.0", diff --git a/readme.md b/readme.md index 5349c75..6d95d13 100644 --- a/readme.md +++ b/readme.md @@ -3,9 +3,9 @@ - Contributors: pfefferle - Donate link: https://notiz.blog/donate/ - Tags: blogroll, podroll, opml, links, feeds -- Requires at least: 6.5 +- Requires at least: 6.8 - Tested up to: 7.1 -- Stable tag: 1.2.0 +- Stable tag: 2.0.0 - Requires PHP: 7.4 - License: GPL-2.0-or-later - License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -66,8 +66,10 @@ this version get their anchor the first time the page is viewed. ### How do I add a link? Paste the address of the site and the plugin fills in the rest: name, description, icon, -and the feed, if it finds one. Everything stays editable, and links can be reordered by -dragging. +and the feed, if it finds one. Every link is a block of its own inside the list, so it +can be moved, duplicated, copied to another page and edited right where it is: name and +description in place, the address behind the name, the feed and the relationship in the +line below, the icon by clicking it. ### Can I import the subscriptions from my feed reader? @@ -127,6 +129,14 @@ routes are described in the Project and support maintained on github at [pfefferle/wordpress-blockroll](https://github.com/pfefferle/wordpress-blockroll). +### 2.0.0 + +* Every link is a block of its own now. You can move it, duplicate it, copy it to another page, and edit it where it shows: the name and the description in the list, the address behind the name, the feed and the relationships in the line below, the image by clicking it. +* Adding a link looks the site up and fills in what it finds, and says so when the site is already in the list. +* Lists made before this version keep working as they are. They get their links as blocks the first time you open the page in the editor, and the file of a page nobody opens again is rendered the way it always was. +* The list can be styled with the colors, spacing and typography of your theme, like any other block. +* Needs WordPress 6.8 or newer. + ### 1.2.0 * Every list has an anchor of its own now, made from its name, so you can link to one blogroll on a page: `https://example.com/links/#podcasts`. The sorting and paging links jump back to the list instead of the top of the page. diff --git a/src/blogroll/__tests__/deprecated.test.js b/src/blogroll/__tests__/deprecated.test.js new file mode 100644 index 0000000..71968dc --- /dev/null +++ b/src/blogroll/__tests__/deprecated.test.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import { hasLinkBlocks, isLegacyList } from '../utils'; + +const link = ( url ) => ( { name: 'blockroll/link', attributes: { url } } ); +const paragraph = { name: 'core/paragraph', attributes: {} }; +const legacy = { links: [ { url: 'https://a.example/', name: 'A' } ] }; + +describe( 'hasLinkBlocks', () => { + it( 'counts link blocks only', () => { + expect( hasLinkBlocks( [ link( 'https://a.example/' ) ] ) ).toBe( true ); + expect( hasLinkBlocks( [ paragraph ] ) ).toBe( false ); + expect( hasLinkBlocks( [] ) ).toBe( false ); + expect( hasLinkBlocks( undefined ) ).toBe( false ); + } ); +} ); + +describe( 'isLegacyList', () => { + it( 'is a list with links in the attribute and none as blocks', () => { + expect( isLegacyList( legacy, [] ) ).toBe( true ); + } ); + + it( 'is one even when something else is in it', () => { + expect( isLegacyList( legacy, [ paragraph ] ) ).toBe( true ); + } ); + + it( 'is not one when the links are blocks already', () => { + expect( isLegacyList( legacy, [ link( 'https://b.example/' ) ] ) ).toBe( + false + ); + } ); + + it( 'is not one without links in the attribute', () => { + expect( isLegacyList( { links: [] }, [] ) ).toBe( false ); + expect( isLegacyList( {}, [] ) ).toBe( false ); + } ); +} ); diff --git a/src/blogroll/__tests__/migrate.test.js b/src/blogroll/__tests__/migrate.test.js new file mode 100644 index 0000000..3972839 --- /dev/null +++ b/src/blogroll/__tests__/migrate.test.js @@ -0,0 +1,32 @@ +import { hasLegacyLinks, migrateLinks } from '../utils'; + +describe( 'hasLegacyLinks', () => { + it( 'is true only for a non-empty links array', () => { + expect( hasLegacyLinks( { links: [ { url: 'x' } ] } ) ).toBe( true ); + expect( hasLegacyLinks( { links: [] } ) ).toBe( false ); + expect( hasLegacyLinks( {} ) ).toBe( false ); + } ); +} ); + +describe( 'migrateLinks', () => { + it( 'empties the attribute and returns the links with an address', () => { + const [ attributes, links ] = migrateLinks( { + anchor: 'blogroll', + sortBy: 'manual', + links: [ + { url: 'https://a.example/', name: 'A' }, + { name: 'no url' }, + { url: 'https://b.example/', xfn: [ 'me' ] }, + ], + } ); + expect( attributes ).toEqual( { + anchor: 'blogroll', + sortBy: 'manual', + links: [], + } ); + expect( links ).toEqual( [ + { url: 'https://a.example/', name: 'A' }, + { url: 'https://b.example/', xfn: [ 'me' ] }, + ] ); + } ); +} ); diff --git a/src/blogroll/__tests__/site-key.test.js b/src/blogroll/__tests__/site-key.test.js new file mode 100644 index 0000000..168832e --- /dev/null +++ b/src/blogroll/__tests__/site-key.test.js @@ -0,0 +1,21 @@ +import { siteKey } from '../utils'; + +describe( 'siteKey', () => { + it( 'ignores scheme, www, case and the trailing slash', () => { + expect( siteKey( 'https://notiz.blog/' ) ).toBe( + siteKey( 'http://www.Notiz.blog' ) + ); + expect( siteKey( 'notiz.blog' ) ).toBe( 'notiz.blog' ); + } ); + + it( 'keeps paths apart', () => { + expect( siteKey( 'https://a.example/blog' ) ).not.toBe( + siteKey( 'https://a.example/' ) + ); + } ); + + it( 'is empty for no address', () => { + expect( siteKey( '' ) ).toBe( '' ); + expect( siteKey( undefined ) ).toBe( '' ); + } ); +} ); diff --git a/src/blogroll/__tests__/to-url.test.js b/src/blogroll/__tests__/to-url.test.js new file mode 100644 index 0000000..40f0029 --- /dev/null +++ b/src/blogroll/__tests__/to-url.test.js @@ -0,0 +1,63 @@ +import { isSafeUrl, toUrl } from '../utils'; + +describe( 'toUrl', () => { + it( 'keeps a full address', () => { + expect( toUrl( 'https://notiz.blog/' ) ).toBe( 'https://notiz.blog/' ); + expect( toUrl( 'http://notiz.blog' ) ).toBe( 'http://notiz.blog' ); + } ); + + it( 'adds https to a bare domain', () => { + expect( toUrl( 'pfefferle.org' ) ).toBe( 'https://pfefferle.org' ); + expect( toUrl( ' pfefferle.org/blog ' ) ).toBe( + 'https://pfefferle.org/blog' + ); + } ); + + it( 'leaves other schemes alone', () => { + expect( toUrl( 'feed:https://a.example/feed/' ) ).toBe( + 'feed:https://a.example/feed/' + ); + } ); +} ); + +describe( 'toUrl with a port', () => { + it( 'treats a port as part of the address, not as a scheme', () => { + expect( toUrl( 'example.com:8080' ) ).toBe( + 'https://example.com:8080' + ); + expect( toUrl( 'localhost:3000/blog' ) ).toBe( + 'https://localhost:3000/blog' + ); + } ); + + it( 'still keeps a real scheme', () => { + expect( toUrl( 'https://example.com:8080/' ) ).toBe( + 'https://example.com:8080/' + ); + expect( toUrl( 'feed:https://a.example/feed/' ) ).toBe( + 'feed:https://a.example/feed/' + ); + } ); +} ); + +describe( 'isSafeUrl', () => { + it( 'takes the web addresses', () => { + expect( isSafeUrl( 'https://a.example/' ) ).toBe( true ); + expect( isSafeUrl( 'http://a.example:8080/x' ) ).toBe( true ); + } ); + + it( 'refuses the others', () => { + expect( isSafeUrl( 'javascript:alert(1)' ) ).toBe( false ); + expect( isSafeUrl( ' JavaScript:alert(1)' ) ).toBe( false ); + expect( isSafeUrl( 'data:text/html,