diff --git a/config/install/patternkit.settings.yml b/config/install/patternkit.settings.yml index 70b58c5..accf773 100644 --- a/config/install/patternkit.settings.yml +++ b/config/install/patternkit.settings.yml @@ -6,3 +6,4 @@ patternkit_json_editor_js: "" patternkit_json_editor_theme: bootstrap4 patternkit_libraries: [] patternkit_render_cache: true +patternkit_pl_host: https://demo.patternlab.io/api diff --git a/patternkit.links.task.yml b/patternkit.links.task.yml index 43d50c5..3630f5b 100644 --- a/patternkit.links.task.yml +++ b/patternkit.links.task.yml @@ -22,3 +22,7 @@ patternkit.settings.json_settings: title: 'JSON Pattern Library Settings' route_name: patternkit.settings.json_settings base_route: patternkit.settings +patternkit.settings.rest_settings: + title: 'REST Pattern Library Settings' + route_name: patternkit.settings.rest_settings + base_route: patternkit.settings diff --git a/patternkit.routing.yml b/patternkit.routing.yml index b9f1814..0625840 100644 --- a/patternkit.routing.yml +++ b/patternkit.routing.yml @@ -55,3 +55,13 @@ patternkit.settings.json_settings: _admin_route: TRUE requirements: _permission: 'access administration pages' +patternkit.settings.rest_settings: + path: '/admin/config/user-interface/patternkit/rest' + defaults: + _form: '\Drupal\patternkit\Form\PatternLibraryRESTForm' + _title: 'Patternkit REST Library settings' + _description: 'Configure Patternkit REST Pattern Library Support.' + options: + _admin_route: TRUE + requirements: + _permission: 'access administration pages' diff --git a/patternkit.services.yml b/patternkit.services.yml index 3b56a04..2d64c16 100644 --- a/patternkit.services.yml +++ b/patternkit.services.yml @@ -20,6 +20,9 @@ services: patternkit.library.discovery.parser.json: class: Drupal\patternkit\PatternLibraryParser\JSONPatternLibraryParser arguments: ['@serialization.json', '@app.root', '@module_handler', '@theme.manager'] + patternkit.library.discovery.parser.rest: + class: Drupal\patternkit\PatternLibraryParser\RESTPatternLibraryParser + arguments: ['@cache.default', '@http_client', '@config.factory', '@file_system', '@logger.channel.patternkit', '@serialization.json', '@stream_wrapper_manager'] patternkit.library.discovery.parser.twig: class: Drupal\patternkit\PatternLibraryParser\TwigPatternLibraryParser arguments: ['@serialization.json', '@app.root', '@module_handler', '@theme.manager'] diff --git a/src/Form/PatternLibraryJSONForm.php b/src/Form/PatternLibraryJSONForm.php index a6b1aac..015d48b 100644 --- a/src/Form/PatternLibraryJSONForm.php +++ b/src/Form/PatternLibraryJSONForm.php @@ -133,4 +133,17 @@ public function getFormId() :string { return 'patternkit_json_editor_config'; } + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config(static::SETTINGS) + ->set('patternkit_json_editor_theme', $form_state->getValue('patternkit_json_editor_theme')) + ->set('patternkit_json_editor_icons', $form_state->getValue('patternkit_json_editor_icons')) + ->set('patternkit_json_editor_css', $form_state->getValue('patternkit_json_editor_css')) + ->set('patternkit_json_editor_js', $form_state->getValue('patternkit_json_editor_js')) + ->save(); + parent::submitForm($form, $form_state); + } + } diff --git a/src/Form/PatternLibraryRESTForm.php b/src/Form/PatternLibraryRESTForm.php new file mode 100644 index 0000000..f4cb0c2 --- /dev/null +++ b/src/Form/PatternLibraryRESTForm.php @@ -0,0 +1,62 @@ +config(static::SETTINGS); + + $form['patternkit_pl_host'] = array( + '#type' => 'textfield', + '#title' => t('PatternLab Host Web Address'), + '#description' => t('Enter the website address of the PatternLab host REST endpoint.'), + '#default_value' => $config->get('patternkit_pl_host'), + ); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritDoc} + */ + protected function getEditableConfigNames() :array { + return [static::SETTINGS]; + } + + /** + * {@inheritDoc} + */ + public function getFormId() :string { + return 'patternkit_rest_editor_config'; + } + + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config(static::SETTINGS) + ->set('patternkit_pl_host', $form_state->getValue('patternkit_pl_host')) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Form/PatternkitSettingsForm.php b/src/Form/PatternkitSettingsForm.php index 3d9e683..4fdf879 100644 --- a/src/Form/PatternkitSettingsForm.php +++ b/src/Form/PatternkitSettingsForm.php @@ -2,6 +2,7 @@ namespace Drupal\patternkit\Form; +use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; @@ -130,8 +131,7 @@ public function getFormId() :string { * {@inheritDoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { - $config = $this->config(static::SETTINGS); - $config + $config = $this->config(static::SETTINGS) ->set('patternkit_libraries', $form_state->getValue('patternkit_libraries')) ->set('patternkit_cache_enabled', $form_state->getValue('patternkit_cache_enabled')) ->set('patternkit_render_cache', $form_state->getValue('patternkit_render_cache')) diff --git a/src/Loader/PatternLibraryLoader.php b/src/Loader/PatternLibraryLoader.php index 4641538..f5b01c6 100644 --- a/src/Loader/PatternLibraryLoader.php +++ b/src/Loader/PatternLibraryLoader.php @@ -2,13 +2,16 @@ namespace Drupal\patternkit\Loader; +use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\patternkit\PatternLibraryCollector; +use Twig\Error\LoaderError; +use Twig_LoaderInterface; /** * Functionality for parsing Twig pattern libraries. */ -class PatternLibraryLoader extends \Twig_Loader_Filesystem { +class PatternLibraryLoader extends \Twig_Loader_Filesystem implements Twig_LoaderInterface { /** * Overrides to add paths from pattern libraries. @@ -21,6 +24,7 @@ class PatternLibraryLoader extends \Twig_Loader_Filesystem { * Provides library names and paths. * * @throws \Twig\Error\LoaderError + * Thrown when encountering a cascaded loader error. */ public function __construct($paths, LoggerChannelInterface $logger, @@ -30,8 +34,10 @@ public function __construct($paths, try { $libraries = $pattern_collector->getLibraryDefinitions(); } - catch (\Exception $exception) { - $logger->error('Error loading pattern libraries: @message', ['@message' => $exception->getMessage()]); + catch (PluginException $exception) { + $message = $exception->getMessage(); + $logger->error('Error loading pattern libraries: @message', ['@message' => $message]); + throw new LoaderError($message); } foreach ($libraries as $namespace => $library) { $path = $library['data']; diff --git a/src/PatternLibraryCollector.php b/src/PatternLibraryCollector.php index c053a13..28cdd77 100644 --- a/src/PatternLibraryCollector.php +++ b/src/PatternLibraryCollector.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Cache\CacheCollector; use Drupal\Core\Lock\LockBackendInterface; +use function is_array; /** * The PatternLibraryCollector caches library information and performs retrieval @@ -129,7 +130,7 @@ public static function create(ContainerInterface $container): self { /** @var \Drupal\Core\Lock\LockBackendInterface $lock */ $lock = $container->get('lock'); /** @var \Drupal\Core\Logger\LoggerChannelInterface $logger */ - $logger = $container->get('@logger.channel.default'); + $logger = $container->get('logger.channel.patternkit'); /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ $module_handler = $container->get('module_handler'); /** @var string $root */ @@ -210,7 +211,7 @@ public function buildByExtension($extension_type, $extension): array { $libraries = $this->applyLibrariesOverride($libraries, $extension); foreach ($libraries as $id => &$library) { - if (!isset($library['patterns'])) { + if (!isset($library['patterns']) || !is_array($library['patterns'])) { unset($libraries[$id]); continue; } diff --git a/src/PatternLibraryParser/FilePatternLibraryParser.php b/src/PatternLibraryParser/FilePatternLibraryParser.php index 6dd73bf..93a0156 100644 --- a/src/PatternLibraryParser/FilePatternLibraryParser.php +++ b/src/PatternLibraryParser/FilePatternLibraryParser.php @@ -10,6 +10,7 @@ use Drupal\patternkit\PatternEditorConfig; use Drupal\patternkit\PatternLibraryJSONParserTrait; use Drupal\patternkit\PatternLibraryParserBase; +use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator; /** * Parses a File pattern library collection into usable metadata. @@ -39,6 +40,50 @@ public function __construct( parent::__construct($root, $module_handler, $theme_manager); } + /** + * Returns an array of file components grouped by file basename and extension. + * + * @param string $path + * The fully-qualified path to discover component files. + * @param array $filter + * An optional filter of file extensions to search for. + * + * @return array + * Array of file components. + * [basename][extension] = filename. + */ + public static function discoverComponents($path, array $filter = []): array { + $components = []; + $rdit = new RecursiveDirectoryIterator($path, \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO); + /** @var \SplFileInfo $file */ + foreach (new \RecursiveIteratorIterator($rdit) as $file) { + // Skip directories and non-files. + if (!$file->isFile()) { + continue; + } + $file_path = $file->getPath(); + + // Skip tests folders. + if (strpos($file_path, '/tests') !== FALSE) { + continue; + } + + // Get the file extension for the file. + $file_ext = $file->getExtension(); + if (!in_array(strtolower($file_ext), $filter, TRUE)) { + continue; + } + + // We use file_basename as a unique key, it is required that the + // JSON and twig file share this basename. + $file_basename = $file->getBasename('.' . $file_ext); + + // Build an array of all the filenames of interest, keyed by name. + $components[$file_basename][$file_ext] = "$file_path/$file_basename.$file_ext"; + } + return $components; + } + /** * Fetches all assets for a pattern. * diff --git a/src/PatternLibraryParser/RESTPatternLibraryParser.php b/src/PatternLibraryParser/RESTPatternLibraryParser.php index d864cb3..4d6aa83 100644 --- a/src/PatternLibraryParser/RESTPatternLibraryParser.php +++ b/src/PatternLibraryParser/RESTPatternLibraryParser.php @@ -1,40 +1,78 @@ cache = $cache; $this->client = $client; + $this->config = $config_factory->get('patternkit'); $this->fs = $fs; + $this->logger = $logger; + $this->serializer = $serializer; + $this->wrapperManager = $wrapperManager; } /** @@ -53,29 +91,29 @@ public function __construct(ConfigFactoryInterface $config_factory, public function fetchPatternAssets(Pattern $pattern, PatternEditorConfig $config) :Pattern { - $patternkit_host = $this->configFactory->get('patternkit_pl_host'); + $patternkit_host = $this->config->get('patternkit_pl_host'); $url = $patternkit_host . '/api/render/json'; $result = $this->client->request( 'GET', $url, - array( - 'headers' => array('Content-Type' => 'application/json'), + [ + 'headers' => ['Content-Type' => 'application/json'], 'data' => $config->rawJSON, 'timeout' => 10, 'method' => 'POST', - ) + ] ); // @TODO: Request failure handling. $body = $result->getBody(); $response = $body->read($body->getSize()); $pk_obj = $this->serializer->deserialize($response, 'object', 'json'); - $subtype = $pattern->subtype; - $dir = "public://patternkit/$subtype/{$config->instance_id}"; + $category = $pattern->category; + $dir = "public://patternkit/$category/{$config->instance_id}"; if (!$this->fs->prepareDirectory($dir)) { // @TODO: Failure handling. - _patternkit_show_error( + $this->logger->error( "Unable to create folder ($dir) to contain the pklugins artifacts." ); } @@ -90,12 +128,12 @@ public function fetchPatternAssets(Pattern $pattern, if ($save_result === FALSE) { // @TODO: Failure handling. - _patternkit_show_error( - "Unable to create static archive of the JSON pklugins artifact for $subtype." + $this->logger->error( + "Unable to create static archive of the JSON pklugins artifact for $category." ); } - $assets = array(); + $assets = []; // Normalize the object for easier processing. if (!empty($pk_obj->assets)) { @@ -108,7 +146,7 @@ public function fetchPatternAssets(Pattern $pattern, $pk_obj->assets->js->early = $pk_obj->global_assets->js; $pk_obj->assets->js->deferred = $pk_obj->global_assets->footer_js; $pk_obj->assets->css->list = $pk_obj->global_assets->css; - $pk_obj->assets->css->shared = array(); + $pk_obj->assets->css->shared = []; } if (!empty($pk_obj->assets)) { @@ -142,8 +180,45 @@ public function fetchPatternAssets(Pattern $pattern, continue; } - $save_result = _patternkit_fetch_single_asset($dir, $pk_obj->path, $asset_url); - $pk_obj->raw_assets[$asset_url] = $save_result; + // Leading double slashes eliminated above, leaving only relatives. + $path = "$dir/" . dirname(trim($asset_url, '.')); + $filename = basename(trim($asset_url, '.')); + + if (!$this->fs->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY)) { + $this->logger->error( + "Unable to create folder ($path) to contain the pklugins artifacts." + ); + } + + // Generate the full path to the source asset. + $full_asset_url = $patternkit_host . preg_replace('/^\\.\\//', $pattern->path . '/', $asset_url); + + // What follows is for store/cache model. + $asset_src = $this->client->request('GET', $full_asset_url); + // May consider some way of doing this + // $dest_path = "$dir/" . md5($asset_src->data) . ".$asset_type";. + $dest_path = $path . $filename; + + $save_result = $this->fs->saveData( + $asset_src->getBody()->getContents(), + $dest_path, + FileSystemInterface::EXISTS_REPLACE + ); + + if ($save_result === FALSE) { + // @todo: handle failure. + continue; + } + + // Convert stream wrapper to relative path, if appropriate. + $scheme = StreamWrapperManagerInterface::getScheme($save_result); + if ($scheme && $this->wrapperManager->isValidScheme($scheme)) { + $wrapper = $this->wrapperManager->getViaScheme($scheme); + $save_result = $wrapper->getDirectoryPath() . "/" . StreamWrapperManagerInterface::getTarget( + $save_result + ); + } + $pattern->raw_assets[$asset_url] = $save_result; } } @@ -151,11 +226,220 @@ public function fetchPatternAssets(Pattern $pattern, // differently. switch ($config->presentation_style) { case 'webcomponent': - _patternkit_fetch_webcomponent_assets($subtype, $config, $pk_obj); + $url = $patternkit_host . '/api/render/webcomponent'; + $result = $this->client->request( + 'GET', + $url, + [ + 'headers' => ['Content-Type' => 'application/json'], + 'jsondata' => $config->rawJSON, + // 'timeout' => 10, + 'method' => 'POST', + ] + ); + + // @todo: Request failure handling. + + // Create the stub object. + $pk_obj = (object) [ + 'PatternkitPattern' => $category, + 'attachments' => [], + 'body' => 'fragment.html', + ]; + + $dir = "public://patternkit/$category/{$config->instance_id}"; + if (!$this->fs->prepareDirectory($dir, $this->fs::CREATE_DIRECTORY)) { + \Drupal::messenger()->addMessage( + t( + 'Unable to create folder or save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ), + \Drupal::messenger()::TYPE_ERROR + ); + $this->logger->error( + 'Unable to create folder or save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ); + } + + // Fetch the body html artifact. + $save_result = $this->fs->saveData( + $result->getBody()->getContents(), + "$dir/body.html", + $this->fs::EXISTS_REPLACE + ); + + // Convert stream wrapper to relative path, if appropriate. + $scheme = $this->wrapperManager::getScheme($save_result); + if ($scheme && $this->wrapperManager->isValidScheme($scheme)) { + $wrapper = $this->wrapperManager->getViaScheme($scheme); + $save_result = $wrapper->getDirectoryPath() . "/" . $this->wrapperManager::getTarget( + $save_result + ); + } + + $pk_obj->body = $save_result; + + $pk_obj->attachments['js']['https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.24/webcomponents.min.js'] = [ + 'type' => 'external', + 'scope' => 'header', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + + // Add the header link rel import. + $pk_obj->attachments['drupal_add_html_head_link'][] = [ + [ + 'rel' => 'import', + 'href' => $pk_obj->body, + ] + ]; + + if ($save_result === FALSE) { + \Drupal::messenger()->addMessage( + t( + 'Unable to save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ), + \Drupal::messenger()::TYPE_ERROR + ); + \Drupal::logger('patternkit')->error( + 'Unable to save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ); + // @todo: Do something. + } break; case 'html': - _patternkit_fetch_fragment_assets($subtype, $config, $pk_obj); + $url = $patternkit_host . '/api/render/html'; + $result = $this->client->request( + 'GET', + $url, + [ + 'headers' => ['Content-Type' => 'application/json'], + 'data' => $config->rawJSON, + // 'timeout' => 10,. + 'method' => 'POST', + ] + ); + + // @todo: Request failure handling. + + $pk_obj->pattern = $category; + + $dir = "public://patternkit/$category/{$config->instance_id}"; + if (!$this->fs->prepareDirectory($dir, $this->fs::CREATE_DIRECTORY)) { + \Drupal::messenger()->addMessage( + t( + 'Unable to create folder or save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ), + \Drupal::messenger()::TYPE_ERROR + ); + \Drupal::logger('patternkit')->error( + 'Unable to create folder or save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ); + } + + // Fetch the body html artifact. + $save_result = $this->fs->saveData( + $result->getBody()->getContents(), + "$dir/body.html", + $this->fs::EXISTS_REPLACE + ); + + // Convert stream wrapper to relative path, if appropriate. + $scheme = $this->wrapperManager::getScheme($save_result); + if ($scheme && $this->wrapperManager->isValidScheme($scheme)) { + $wrapper = $this->wrapperManager->getViaScheme($scheme); + $save_result = $wrapper->getDirectoryPath() . "/" . $this->wrapperManager::getTarget( + $save_result + ); + } + + $pk_obj->body = $save_result; + + if ($save_result === FALSE) { + \Drupal::messenger()->addMessage( + t( + 'Unable to save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ), + \Drupal::messenger()::TYPE_ERROR + ); + \Drupal::logger('patternkit')->error( + 'Unable to save metadata/assets for plugin @plugin', + ['@plugin' => $category] + ); + // @todo: Do something. + } + + foreach (['early', 'deferred'] as $stage) { + foreach ($pk_obj->assets->js->{$stage} as $asset_fragment) { + $path = $pk_obj->raw_assets[$asset_fragment]; + + if (strpos($path, 'public://patternkit/') === 0) { + $pk_obj->attachments['js'][$path] = [ + 'type' => 'file', + 'scope' => $stage === 'early' ? 'header' : 'footer', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + else { + $pk_obj->attachments['js'][$path] = [ + 'type' => 'external', + 'scope' => $stage === 'early' ? 'header' : 'footer', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + } + } + + foreach ($pk_obj->assets->css->list as $asset_fragment) { + $path = $pk_obj->raw_assets[$asset_fragment]; + + if (strpos($path, 'public://patternkit/') === 0) { + $pk_obj->attachments['css'][$path] = [ + 'type' => 'file', + 'scope' => 'header', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + else { + $pk_obj->attachments['css'][$path] = [ + 'type' => 'external', + 'scope' => 'header', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + } + + foreach ($pk_obj->assets->css->shared as $asset_fragment) { + $path = $pk_obj->raw_assets[$asset_fragment->src]; + + if (strpos($path, 'public://patternkit/') === 0) { + $pk_obj->attachments['css'][$path] = [ + 'type' => 'file', + 'scope' => 'header', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + else { + $pk_obj->attachments['css'][$path] = [ + 'type' => 'external', + 'scope' => 'header', + 'group' => JS_DEFAULT, + 'weight' => 0, + ]; + } + } break; case 'json': @@ -199,13 +483,13 @@ public function getTitle() :string { * The renderable pattern editor. */ public function getEditor($subtype = NULL, PatternEditorConfig $config = NULL) { - $patternkit_host = $this->configFactory->get('patternkit_pl_host'); + $patternkit_host = $this->config->get('patternkit_pl_host'); $url = $patternkit_host . '/schema/editor/' . substr($subtype, 3); if ($config !== NULL) { $url .= !empty($config->lzstring) ? '?data=' . $config->lzstring : ''; } - // @todo Move to external phptemplate. + // @todo Move to external template. $markup = <<< HTML +HTML; + + return $markup; } /** * {@inheritDoc} */ - public function getMetadata($extension, $path): array { - // @todo Implement getMetadata() method. - return []; + public function getMetadata($extension, $library, $path): array { + $patternkit_host = $this->configFactory->get('patternkit')->get('patternkit_pl_host') ?: 'http://localhost:9001'; + + $patterns = \Drupal::httpClient()->request( + 'GET', + $patternkit_host . '/api/patterns', + [ + 'headers' => array('Content-Type' => 'application/json'), + 'timeout' => 10, + ] + ); + if ($patterns === NULL + || !empty($patterns->error) + || (int) $patterns->code !== 200) { + \Drupal::logger('patternkit')->error( + 'Patternkit failed to load metadata from service (%service_uri): %error', + [ + '%service_uri' => $patternkit_host . '/api/patterns', + '%error' => !empty($patterns->error) ? $patterns->error : $patterns->code, + ] + ); + return []; + } + $metadata = (array) $this->serializer::decode($patterns->data); + foreach ($metadata as $subtype => $pattern) { + $pattern->library = &$this; + $metadata[$subtype] = $pattern; + } + return $metadata; } /**