Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,13 @@ Configured under **Settings → Crumb**. Settings can be overridden per-shortcod

By default, meeting detail URLs use hash-based routing (`#/monday-night-meeting-42`). To enable clean URLs like `/meetings/monday-night-meeting-42`, enter the page slug (e.g. `meetings`) in the **Base Path for Pretty URLs** setting. After saving, go to **Settings → Permalinks** and click **Save Changes** to update rewrite rules.

## Switching from Crouton

Crumb is an alternative to [crouton](https://wordpress.org/plugins/crouton/) and can drop in without page edits. Just install Crumb and deactivate crouton — existing pages keep working.

* Crouton shortcodes `[bmlt_tabs]`, `[bmlt_map]`, `[crouton_tabs]`, and `[crouton_map]` are registered automatically and translated to the Crumb widget (map shortcodes render with `view="both"`, tabs with `view="list"`).
* Shortcode attributes `root_server`, `service_body`, `service_body_1`, `formats`, and `report_update_url` are mapped to their Crumb equivalents.
* Crouton's saved settings (server URL, service body, format IDs, update URL) are used as fallbacks whenever the corresponding Crumb setting is empty.
* If both plugins are active, crouton continues to handle its own shortcodes; Crumb only takes over once crouton is deactivated.

Full documentation at **[crumb.bmlt.app](https://crumb.bmlt.app/)**.
146 changes: 135 additions & 11 deletions crumb.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Crumb
* Plugin URI: https://wordpress.org/plugins/crumb/
* Description: Embeds the Crumb meeting finder widget on any page or post using a shortcode.
* Version: 1.3.0
* Version: 1.3.1
* Author: bmltenabled
* Author URI: https://bmlt.app
* License: GPL v2 or later
Expand All @@ -15,17 +15,31 @@
exit;
}

define( 'CRUMB_VERSION', '1.3.0' );
define( 'CRUMB_VERSION', '1.3.1' );

class Crumb {

private static ?self $instance = null;
private static ?bool $shortcode_geolocation = null;
private static ?int $shortcode_geolocation_radius = null;
private static ?array $crouton_options = null;
private static array $compat_tags = [];

const DEFAULT_CDN_URL = 'https://cdn.aws.bmlt.app/crumb-widget.js';
const REWRITE_VERSION = '1';

/**
* Tag → forced view for crouton-named shortcodes.
* Map-flavored tags become "both" (map + list) so users don't lose the list view
* they previously saw next to the map. Tabs become plain list.
*/
const CROUTON_VIEW_MAP = [
'crouton_map' => 'both',
'crouton_tabs' => 'list',
'bmlt_map' => 'both',
'bmlt_tabs' => 'list',
];

public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
Expand All @@ -40,6 +54,8 @@ private function __construct() {
add_action( 'admin_menu', [ static::class, 'admin_menu' ] );
add_action( 'admin_init', [ static::class, 'register_settings' ] );
add_action( 'init', [ static::class, 'init_rewrite_rules' ] );
// Priority 20 so this runs after crouton (if active) has registered its own shortcodes.
add_action( 'init', [ static::class, 'register_crouton_shortcodes' ], 20 );
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ static::class, 'settings_link' ] );
}

Expand Down Expand Up @@ -90,6 +106,99 @@ public static function init_rewrite_rules(): void {
}
}

// -------------------------------------------------------------------------
// Crouton Compatibility
//
// Lets sites running the crouton plugin switch to crumb without editing
// pages. Crouton-named shortcodes are mapped to [crumb] output, and crouton's
// saved settings (bmlt_tabs_options) are used as a fallback whenever the
// matching crumb_* option is empty.
// -------------------------------------------------------------------------

private static function crouton_options(): array {
if ( null === self::$crouton_options ) {
$opts = get_option( 'bmlt_tabs_options', [] );
self::$crouton_options = is_array( $opts ) ? $opts : [];
}
return self::$crouton_options;
}

/**
* Map a crumb option key to the matching value from crouton's bmlt_tabs_options.
* Returns '' if there is no equivalent or it's unset.
*/
private static function crouton_fallback_value( string $crumb_key ): string {
$opts = self::crouton_options();
switch ( $crumb_key ) {
case 'crumb_server':
return isset( $opts['root_server'] ) ? (string) $opts['root_server'] : '';
case 'crumb_service_body':
if ( ! empty( $opts['service_bodies'] ) && is_array( $opts['service_bodies'] ) ) {
return implode( ',', array_map( 'intval', $opts['service_bodies'] ) );
}
return isset( $opts['service_body'] ) ? (string) $opts['service_body'] : '';
case 'crumb_format_ids':
return isset( $opts['formats'] ) ? (string) $opts['formats'] : '';
case 'crumb_update_url':
return isset( $opts['report_update_url'] ) ? (string) $opts['report_update_url'] : '';
}
return '';
}

/**
* Like get_option(), but falls back to the equivalent key in bmlt_tabs_options
* when the crumb option is empty/missing. Final fallback is $default.
*/
public static function get_option_or_crouton( string $crumb_key, string $default = '' ): string {
$value = get_option( $crumb_key, '' );
if ( '' !== trim( (string) $value ) ) {
return (string) $value;
}
$fallback = self::crouton_fallback_value( $crumb_key );
return '' !== $fallback ? $fallback : $default;
}

public static function register_crouton_shortcodes(): void {
foreach ( self::CROUTON_VIEW_MAP as $tag => $view ) {
if ( shortcode_exists( $tag ) ) {
continue;
}
self::$compat_tags[] = $tag;
add_shortcode(
$tag,
static function ( $atts ) use ( $view ) {
return self::crouton_compat_shortcode( $atts, $view );
}
);
}
}

public static function crouton_compat_shortcode( $atts, string $view ): string {
$atts = is_array( $atts ) ? $atts : [];
$translated = [ 'view' => $view ];

if ( isset( $atts['root_server'] ) ) {
$translated['server'] = $atts['root_server'];
}
if ( isset( $atts['service_body'] ) ) {
$translated['service_body'] = $atts['service_body'];
} elseif ( isset( $atts['service_body_1'] ) ) {
$translated['service_body'] = $atts['service_body_1'];
}
if ( isset( $atts['formats'] ) ) {
$translated['format_ids'] = $atts['formats'];
}
if ( isset( $atts['report_update_url'] ) ) {
$translated['update_url'] = $atts['report_update_url'];
}

return self::setup_shortcode( $translated );
}

public static function compat_tags(): array {
return self::$compat_tags;
}

// -------------------------------------------------------------------------
// Shortcode
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -123,18 +232,19 @@ public static function setup_shortcode( array $atts ): string {
}

// Shortcode attribute takes precedence; fall back to saved option only when not provided.
$server = esc_url( trim( $atts['server'] ?? get_option( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ) );
// Crouton settings (bmlt_tabs_options) are used as a fallback when the crumb option is empty.
$server = esc_url( trim( $atts['server'] ?? self::get_option_or_crouton( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ) );

if ( empty( $server ) ) {
return '<p style="color:red"><strong>Crumb:</strong> a <code>server</code> URL is required.</p>';
}

// null → not in shortcode, use saved option.
// '' → explicitly set to empty in shortcode, omit data-service-body (show all meetings).
$service_body = $atts['service_body'] ?? get_option( 'crumb_service_body', '1047,1048' );
$service_body = $atts['service_body'] ?? self::get_option_or_crouton( 'crumb_service_body', '1047,1048' );

// null → not in shortcode, use saved option. '' → omit (no format lock).
$format_ids = $atts['format_ids'] ?? get_option( 'crumb_format_ids', '' );
$format_ids = $atts['format_ids'] ?? self::get_option_or_crouton( 'crumb_format_ids', '' );

// Resolve view: shortcode attr → saved option → omit (widget uses its own default).
$view_raw = $atts['view'] ?? get_option( 'crumb_view', '' );
Expand All @@ -161,7 +271,7 @@ public static function setup_shortcode( array $atts ): string {
}

// null → not in shortcode, use saved option. '' → omit (no update link).
$update_url = $atts['update_url'] ?? get_option( 'crumb_update_url', '' );
$update_url = $atts['update_url'] ?? self::get_option_or_crouton( 'crumb_update_url', '' );
if ( '' !== $update_url ) {
$div .= ' data-update-url="' . esc_attr( trim( $update_url ) ) . '"';
}
Expand All @@ -187,7 +297,21 @@ public static function setup_shortcode( array $atts ): string {

public static function assets(): void {
global $post;
if ( ! $post || ! has_shortcode( $post->post_content, 'crumb' ) ) {
if ( ! $post ) {
return;
}

// Check for [crumb] plus any crouton-named tags this plugin actually registered.
// (Tags claimed by an active crouton plugin won't be in self::$compat_tags.)
$tags = array_merge( [ 'crumb' ], self::$compat_tags );
$found = false;
foreach ( $tags as $tag ) {
if ( has_shortcode( $post->post_content, $tag ) ) {
$found = true;
break;
}
}
if ( ! $found ) {
return;
}

Expand Down Expand Up @@ -427,7 +551,7 @@ public static function settings_page(): void {
<th scope="row"><label for="crumb_server">BMLT Server URL</label></th>
<td>
<input type="url" id="crumb_server" name="crumb_server"
value="<?php echo esc_attr( get_option( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ); ?>"
value="<?php echo esc_attr( self::get_option_or_crouton( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ); ?>"
class="regular-text" placeholder="https://your-server/main_server" />
<p class="description">Required. The full URL to your BMLT Server.</p>
</td>
Expand All @@ -436,7 +560,7 @@ class="regular-text" placeholder="https://your-server/main_server" />
<th scope="row"><label for="crumb_service_body">Service Body IDs</label></th>
<td>
<input type="text" id="crumb_service_body" name="crumb_service_body"
value="<?php echo esc_attr( get_option( 'crumb_service_body', '1047,1048' ) ); ?>"
value="<?php echo esc_attr( self::get_option_or_crouton( 'crumb_service_body', '1047,1048' ) ); ?>"
class="regular-text" placeholder="42 or 42,57,103" />
<p class="description">Optional. Single ID or comma-separated list. Leave empty to show all meetings. Child service bodies are always included.</p>
</td>
Expand All @@ -445,7 +569,7 @@ class="regular-text" placeholder="42 or 42,57,103" />
<th scope="row"><label for="crumb_format_ids">Format IDs</label></th>
<td>
<input type="text" id="crumb_format_ids" name="crumb_format_ids"
value="<?php echo esc_attr( get_option( 'crumb_format_ids', '' ) ); ?>"
value="<?php echo esc_attr( self::get_option_or_crouton( 'crumb_format_ids', '' ) ); ?>"
class="regular-text" placeholder="17 or 17,54,78" />
<p class="description">Optional. Single ID or comma-separated list of BMLT format IDs to lock the widget to. Leave empty to show all formats. Can be overridden per-page via the shortcode <code>format_ids</code> attribute.</p>
</td>
Expand Down Expand Up @@ -509,7 +633,7 @@ class="small-text" placeholder="-50" />
<th scope="row"><label for="crumb_update_url">Update Meeting URL</label></th>
<td>
<input type="text" id="crumb_update_url" name="crumb_update_url"
value="<?php echo esc_attr( get_option( 'crumb_update_url', '' ) ); ?>"
value="<?php echo esc_attr( self::get_option_or_crouton( 'crumb_update_url', '' ) ); ?>"
class="large-text" placeholder="https://example.org/meeting-update-form/?meeting_id={meeting_id}" />
<p class="description">
Optional. URL template for the <strong>Update Meeting Info</strong> link shown at the bottom of the meeting detail panel.
Expand Down
22 changes: 21 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags: narcotics anonymous, na, meetings, bmlt, meeting finder
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.1
Stable tag: 1.3.0
Stable tag: 1.3.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -42,6 +42,15 @@ Shortcode attributes:
* `geolocation_radius` — Geolocation search radius. Positive integer = fixed radius in miles (or km per server settings). Negative integer = BMLT auto-radius: the server expands the search until it finds roughly that many meetings (e.g. `-50` finds ~50 nearby meetings). Overrides the Geolocation Radius setting and Widget Configuration.
* `update_url` — URL template for the **Update Meeting Info** link shown at the bottom of the meeting detail panel. Supports tokens `{meeting_id}`, `{meeting_name}`, `{server_url}`, `{return_url}` (URL-encoded on substitution). Works with bmlt-workflow, hosted forms, or `mailto:` URLs.

= Switching from Crouton =

Crumb is an alternative to the [crouton](https://wordpress.org/plugins/crouton/) plugin and can drop in without page edits in most cases. Activating Crumb will:

* Register the crouton shortcodes (`[bmlt_tabs]`, `[bmlt_map]`, `[crouton_tabs]`, `[crouton_map]`) and translate them to the Crumb widget. Map shortcodes render with `view="both"` (map + list) and tabs shortcodes render with `view="list"`. Shortcode attributes `root_server`, `service_body`, `service_body_1`, `formats`, and `report_update_url` are mapped to their Crumb equivalents.
* Reuse crouton's saved settings as fallbacks when the corresponding Crumb option is empty (BMLT server URL, service bodies, format IDs, update URL). Open **Settings → Crumb** to confirm the inherited values and click **Save Changes** to persist them.

Crumb only handles those shortcodes when crouton is deactivated — if both plugins are active, crouton continues to handle its own shortcodes. To switch: install Crumb, then deactivate crouton. No page edits required.

= Documentation =

Full documentation at [crumb.bmlt.app](https://crumb.bmlt.app/).
Expand Down Expand Up @@ -79,6 +88,10 @@ Yes. Append `?view=list` or `?view=map` to any page URL that contains the `[crum

The shortcode works in any context that processes WordPress shortcodes. If your page builder does not render shortcodes automatically, use its dedicated shortcode block.

= I'm switching from the crouton plugin — do I have to edit my pages? =

No. Crumb registers the crouton shortcodes (`[bmlt_tabs]`, `[bmlt_map]`, `[crouton_tabs]`, `[crouton_map]`) and renders them with the Crumb widget. It also reads crouton's saved settings (server URL, service body, format IDs, update URL) as fallbacks. Just install Crumb, deactivate crouton, and existing pages keep working. See the **Switching from Crouton** section above for details.

== External services ==

This plugin relies on two external services. Both are part of the BMLT (Basic Meeting List Toolkit) ecosystem — free, open-source tools built for Narcotics Anonymous service bodies (https://bmlt.app).
Expand Down Expand Up @@ -111,6 +124,10 @@ The widget fetches meeting data from a BMLT server whose URL you configure in Se

== Changelog ==

= 1.3.1 =
* Added compatibility layer for the [crouton](https://wordpress.org/plugins/crouton/) plugin. Crumb now registers `[bmlt_tabs]`, `[bmlt_map]`, `[crouton_tabs]`, and `[crouton_map]` shortcodes and renders them with the Crumb widget when crouton is not active. Shortcode attributes (`root_server`, `service_body`, `service_body_1`, `formats`, `report_update_url`) are translated automatically.
* Crumb falls back to crouton's saved settings (server URL, service body, format IDs, update URL) when the corresponding Crumb option is empty — installing and activating is enough; no page or settings edits required.

= 1.3.0 =
* Added **Update Meeting URL** setting and `update_url` shortcode attribute — URL template that powers the "Update Meeting Info" link on the meeting detail panel. Supports tokens `{meeting_id}`, `{meeting_name}`, `{server_url}`, and `{return_url}` (URL-encoded on substitution). Works with [bmlt-workflow](https://github.com/bmlt-enabled/bmlt-workflow), arbitrary hosted forms, or `mailto:` URLs. Leave empty to hide the link.

Expand Down Expand Up @@ -147,6 +164,9 @@ The widget fetches meeting data from a BMLT server whose URL you configure in Se

== Upgrade Notice ==

= 1.3.1 =
Adds drop-in compatibility for the crouton plugin: crouton shortcodes (`[bmlt_tabs]`, `[bmlt_map]`, `[crouton_tabs]`, `[crouton_map]`) now render with the Crumb widget and crouton's saved settings are used as fallbacks. Safe to update.

= 1.3.0 =
Adds Update Meeting URL setting and `update_url` shortcode attribute for the configurable "Update Meeting Info" link. Safe to update.

Expand Down
Loading