public function parse($content)
    {
        $this->extend('onBeforeParse', $content);

        $continue = true;

        // If no shortcodes defined, don't try and parse any
        if (!$this->shortcodes) {
            $continue = false;
        } elseif (!trim($content)) {
            // If no content, don't try and parse it
            $continue = false;
        } elseif (strpos($content, '[') === false) {
            // If no shortcode tag, don't try and parse it
            $continue = false;
        }

        if ($continue) {
            // First we operate in text mode, replacing any shortcodes with marker elements so that later we can
            // use a proper DOM
            list($content, $tags) = $this->replaceElementTagsWithMarkers($content);

        /** @var HTMLValue $htmlvalue */
            $htmlvalue = Injector::inst()->create('HTMLValue', $content);

            // Now parse the result into a DOM
            if (!$htmlvalue->isValid()) {
                if (self::$error_behavior == self::ERROR) {
                    user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERROR);
                } else {
                    $continue = false;
                }
            }
        }

        if ($continue) {
            // First, replace any shortcodes that are in attributes
            $this->replaceAttributeTagsWithContent($htmlvalue);

            // Find all the element scoped shortcode markers
            $shortcodes = $htmlvalue->query('//img[@class="' . self::$marker_class . '"]');

            // Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise
            $parents = $this->findParentsForMarkers($shortcodes);

        /** @var DOMElement $shortcode */
            foreach ($shortcodes as $shortcode) {
                $tag = $tags[$shortcode->getAttribute('data-tagid')];
                $parent = $parents[$shortcode->getAttribute('data-parentid')];

                $class = null;
                if (!empty($tag['attrs']['location'])) {
                    $class = $tag['attrs']['location'];
                } elseif (!empty($tag['attrs']['class'])) {
                    $class = $tag['attrs']['class'];
                }

                $location = self::INLINE;
                if ($class == 'left' || $class == 'right') {
                    $location = self::BEFORE;
                }
                /**
                 * Below code disabled due to https://github.com/silverstripe/silverstripe-framework/issues/5987
                if ($class == 'center' || $class == 'leftAlone') {
                    $location = self::SPLIT;
                }
                 */

                if (!$parent) {
                    if ($location !== self::INLINE) {
                        user_error(
                            "Parent block for shortcode couldn't be found, but location wasn't INLINE",
                            E_USER_ERROR
                        );
                    }
                } else {
                    $this->moveMarkerToCompliantHome($shortcode, $parent, $location);
                }

                $this->replaceMarkerWithContent($shortcode, $tag);
            }

            $content = $htmlvalue->getContent();

            // Clean up any marker classes left over, for example, those injected into <script> tags
            $parser = $this;
            $content = preg_replace_callback(
                // Not a general-case parser; assumes that the HTML generated in replaceElementTagsWithMarkers()
                // hasn't been heavily modified
                '/<img[^>]+class="' . preg_quote(self::$marker_class) . '"[^>]+data-tagid="([^"]+)"[^>]*>/i',
                function ($matches) use ($tags, $parser) {
                    $tag = $tags[$matches[1]];
                    return $parser->getShortcodeReplacementText($tag);
                },
                $content
            );
        }

        $this->extend('onAfterParse', $content);

        return $content;
    }