/*
 * Copyright (C) 2019 Canonical Ltd.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2 or version 3 of the License.
 * See http://www.gnu.org/copyleft/lgpl.html the full text of the license.
 */

#include <ctype.h>
#include <string.h>

#include "snapd-markdown-node.h"
#include "snapd-markdown-parser.h"

/**
 * SECTION:snapd-markdown-parser
 * @short_description: Snap markdown text parser
 * @include: snapd-glib/snapd-glib.h
 *
 * A #SnapdMarkdownParser parses text formatted in markdown; for example the
 * text returned by snapd_snap_get_description ().
 *
 * Snap supports the following subset of CommonMark (https://commonmark.org):
 * - Indented Code Blocks
 * - Paragraphs
 * - Blank Lines
 * - Unordered Lists
 * - Backslash escapes
 * - Code spans
 *
 * In addition text that contains URLs are converted into links.
 *
 * Use snapd_markdown_parser_parse () to convert text into a tree of
 * #SnapdMarkdownNode that you can then process to display in your client.
 */

/**
 * SnapdMarkdownParser:
 *
 * #SnapdMarkdownParser is an opaque data structure and can only be accessed
 * using the provided functions.
 *
 * Since: 1.48
 */
struct _SnapdMarkdownParser {
  GObject parent_instance;

  gboolean preserve_whitespace;
};

G_DEFINE_TYPE(SnapdMarkdownParser, snapd_markdown_parser, G_TYPE_OBJECT)

static gboolean parse_empty_line(const gchar *line) {
  for (int i = 0; line[i] != '\0'; i++)
    if (!isspace(line[i]))
      return FALSE;
  return TRUE;
}

static gboolean parse_paragraph(const gchar *line, gchar **text) {
  int i = 0;
  while (isspace(line[i]))
    i++;

  if (text != NULL)
    *text = g_strdup(line + i);

  return TRUE;
}

static gboolean parse_bullet_list_item(const gchar *line, int *offset,
                                       gchar *symbol, gchar **bullet_text) {
  int i = 0;
  while (isspace(line[i]))
    i++;
  gchar symbol_ = line[i];
  if (symbol_ != '-' && symbol_ != '+' && symbol_ != '*')
    return FALSE;
  int marker_offset = i;
  i++;
  if (line[i] == '\0')
    return FALSE;

  if (!isspace(line[i]))
    return FALSE;
  i++;

  int offset_ = i;
  while (isspace(line[offset_]))
    offset_++;

  /* Blank lines start one place after marker */
  if (line[offset_] == '\0')
    offset_ = marker_offset + 1;

  if (offset != NULL)
    *offset = offset_;
  if (symbol != NULL)
    *symbol = symbol_;
  if (bullet_text != NULL)
    *bullet_text = g_strdup(line + i);

  return TRUE;
}

static gboolean parse_list_item_line(const gchar *line, int offset,
                                     gchar **text) {
  for (int i = 0; i < offset; i++) {
    if (!isspace(line[i]))
      return FALSE;
  }

  if (text != NULL)
    *text = g_strdup(line + offset);

  return TRUE;
}

static gboolean parse_indented_code_block(const gchar *line, gchar **text) {
  int space_count = 0;
  while (line[space_count] == ' ')
    space_count++;
  if (space_count < 4)
    return FALSE;

  if (text != NULL)
    *text = g_strdup(line + 4);

  return TRUE;
}

static gboolean is_punctuation_character(gchar c) {
  return strchr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", c) != NULL;
  // FIXME: Also support unicode categories Pc, Pd, Pe, Pf, Pi, Po, and Ps.
}

static gboolean is_left_flanking_delimiter_run(const gchar *text, int index) {
  if (text[index] != '*' && text[index] != '_')
    return FALSE;

  int run_length = 1;
  while (text[index + run_length] == text[index])
    run_length++;

  /* 1) Must not be followed by whitespace */
  if (text[index + run_length] == '\0')
    return FALSE;
  if (isspace(text[index + run_length]))
    return FALSE;

  /* 2a) Is not followed by punctuation */
  if (!is_punctuation_character(text[index + run_length]))
    return TRUE;

  /* 2b) Followed by punctuation and preceeded by whitespace or punctuation */
  if (index == 0 || isspace(text[index - 1]))
    return TRUE;
  if (is_punctuation_character(text[index - 1]))
    return TRUE;

  return FALSE;
}

static gboolean is_right_flanking_delimiter_run(const gchar *text, int index) {
  if (text[index] != '*' && text[index] != '_')
    return FALSE;

  int run_length = 1;
  while (text[index + run_length] == text[index])
    run_length++;

  /* 1) Not preceeded by whitespace */
  if (index == 0 || isspace(text[index - 1]))
    return FALSE;

  /* 2a) No preceeded by punctuation */
  if (!is_punctuation_character(text[index - 1]))
    return TRUE;

  /* 2b) Preceeded by punctuation and followed by whitespace or punctuation */
  if (text[index + run_length] == '\0' || isspace(text[index + run_length]))
    return TRUE;
  if (is_punctuation_character(text[index + run_length]))
    return TRUE;

  return FALSE;
}

static gchar *strip_text(const gchar *text) {
  g_autoptr(GString) stripped_text = g_string_new("");

  /* Strip leading whitespace */
  int i = 0;
  while (isspace(text[i]))
    i++;

  gboolean in_whitespace = FALSE;
  while (text[i] != '\0') {
    if (isspace(text[i]))
      in_whitespace = TRUE;
    else {
      if (in_whitespace)
        g_string_append_c(stripped_text, ' ');
      in_whitespace = FALSE;
      g_string_append_c(stripped_text, text[i]);
    }
    i++;
  }

  return g_steal_pointer(&stripped_text->str);
}

typedef struct {
  gchar character;
  int length;
  gboolean can_open_emphasis;
  gboolean can_close_emphasis;
} EmphasisInfo;

static EmphasisInfo *emphasis_info_new(void) { return g_new0(EmphasisInfo, 1); }

static void emphasis_info_free(EmphasisInfo *info) { g_free(info); }

G_DEFINE_AUTOPTR_CLEANUP_FUNC(EmphasisInfo, emphasis_info_free)

SnapdMarkdownNode *make_text_node(const gchar *text, int length) {
  if (length > 0) {
    g_autofree gchar *subtext = g_strndup(text, length);
    return g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                        SNAPD_MARKDOWN_NODE_TYPE_TEXT, "text", subtext, NULL);
  } else
    return g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                        SNAPD_MARKDOWN_NODE_TYPE_TEXT, "text", text, NULL);
}

SnapdMarkdownNode *make_paragraph_text_node(SnapdMarkdownParser *self,
                                            const gchar *text, int length) {
  if (self->preserve_whitespace)
    return make_text_node(text, length);

  g_autoptr(GString) result = g_string_new("");
  gchar last_c = '\0';
  for (int i = 0; text[i] != '\0' && (length == -1 || i < length); i++) {
    gchar c = text[i];
    if (isspace(c)) {
      if (!isspace(last_c))
        g_string_append_c(result, ' ');
    } else
      g_string_append_c(result, c);
    last_c = c;
  }

  return make_text_node(result->str, -1);
}

SnapdMarkdownNode *make_delimiter_node(EmphasisInfo *info) {
  g_autofree gchar *text = g_malloc(info->length + 1);
  for (int i = 0; i < info->length; i++)
    text[i] = info->character;
  text[info->length] = '\0';

  return g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                      SNAPD_MARKDOWN_NODE_TYPE_TEXT, "text", text, NULL);
}

SnapdMarkdownNode *make_code_node(SnapdMarkdownNodeType type,
                                  const gchar *text) {
  g_autoptr(GPtrArray) children =
      g_ptr_array_new_with_free_func(g_object_unref);
  g_ptr_array_add(children, make_text_node(text, -1));
  return g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type", type, "children",
                      children, NULL);
}

static void find_emphasis(GPtrArray *nodes, GHashTable *emphasis_info) {
  for (int end_index = 0; end_index < nodes->len; end_index++) {
    SnapdMarkdownNode *end_node = g_ptr_array_index(nodes, end_index);
    EmphasisInfo *end_info = g_hash_table_lookup(emphasis_info, end_node);
    if (end_info == NULL || !end_info->can_close_emphasis)
      continue;

    /* Find a start emphasis that matches this end */
    int start_index;
    SnapdMarkdownNode *start_node;
    EmphasisInfo *start_info;
    for (start_index = end_index - 1; start_index >= 0; start_index--) {
      start_node = g_ptr_array_index(nodes, start_index);
      start_info = g_hash_table_lookup(emphasis_info, start_node);
      if (start_info != NULL && start_info->can_open_emphasis &&
          start_info->character == end_info->character)
        break;
    }
    if (start_index < 0)
      continue;

    // FIXME: Can do if both a multiple of three
    if ((start_info->can_open_emphasis && start_info->can_close_emphasis) ||
        (end_info->can_open_emphasis && end_info->can_close_emphasis)) {
      if ((start_info->length + end_info->length) % 3 == 0)
        continue;
    }

    g_assert(start_info->length > 0);
    g_assert(end_info->length > 0);

    SnapdMarkdownNodeType node_type;
    if (start_info->length > 1 && end_info->length > 1) {
      node_type = SNAPD_MARKDOWN_NODE_TYPE_STRONG_EMPHASIS;
      start_info->length -= 2;
      end_info->length -= 2;
    } else {
      node_type = SNAPD_MARKDOWN_NODE_TYPE_EMPHASIS;
      start_info->length--;
      end_info->length--;
    }

    /* Replace nodes */
    g_autoptr(GPtrArray) children =
        g_ptr_array_new_with_free_func(g_object_unref);
    for (int i = start_index + 1; i < end_index; i++) {
      SnapdMarkdownNode *node = g_ptr_array_index(nodes, i);
      g_ptr_array_add(children, g_object_ref(node));
    }
    g_ptr_array_remove_range(nodes, start_index, end_index - start_index + 1);
    g_ptr_array_insert(nodes, start_index,
                       g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                                    node_type, "children", children, NULL));
    g_hash_table_steal(emphasis_info, start_node);
    g_hash_table_steal(emphasis_info, end_node);
    if (end_info->length > 0) {
      SnapdMarkdownNode *node = make_delimiter_node(end_info);
      g_hash_table_insert(emphasis_info, node, end_info);
      g_ptr_array_insert(nodes, start_index + 1, node);
    } else
      emphasis_info_free(end_info);
    if (start_info->length > 0) {
      SnapdMarkdownNode *node = make_delimiter_node(start_info);
      g_hash_table_insert(emphasis_info, node, start_info);
      g_ptr_array_insert(nodes, start_index, node);
    } else
      emphasis_info_free(start_info);
    end_index = start_index;
  }
}

static int backtick_count(const gchar *text) {
  int count = 0;
  while (text[count] == '`')
    count++;
  return count;
}

static void combine_text_nodes(GPtrArray *nodes) {
  for (int i = 0; i < nodes->len; i++) {
    SnapdMarkdownNode *node = g_ptr_array_index(nodes, i);

    GPtrArray *children = snapd_markdown_node_get_children(node);
    if (children != NULL)
      combine_text_nodes(children);

    if (snapd_markdown_node_get_node_type(node) !=
        SNAPD_MARKDOWN_NODE_TYPE_TEXT)
      continue;

    int node_count = 1;
    g_autoptr(GString) text = NULL;
    while (i + node_count < nodes->len) {
      SnapdMarkdownNode *n = g_ptr_array_index(nodes, i + node_count);
      if (snapd_markdown_node_get_node_type(n) != SNAPD_MARKDOWN_NODE_TYPE_TEXT)
        break;
      if (text == NULL)
        text = g_string_new(snapd_markdown_node_get_text(node));
      g_string_append(text, snapd_markdown_node_get_text(n));
      node_count++;
    }

    if (node_count > 1) {
      g_ptr_array_remove_range(nodes, i, node_count);
      g_ptr_array_insert(nodes, i, make_text_node(text->str, -1));
    }
  }
}

static gboolean is_valid_url_char(gchar c) {
  if ((c & 0x80) != 0)
    return TRUE;

  if (c >= 'a' && c <= 'z')
    return TRUE;
  if (c >= 'A' && c <= 'Z')
    return TRUE;
  if (c >= '0' && c <= '9')
    return TRUE;
  /* "Safe" characters */
  if (strchr("$-_.+", c) != NULL)
    return TRUE;
  /* "Reserved" characters */
  if (strchr(";/?:@&=", c) != NULL)
    return TRUE;
  if (strchr("~#[]!'()*,%", c) != NULL)
    return TRUE;

  return FALSE;
}

static gboolean is_url(const gchar *text, int *length) {
  int prefix_length;
  if (g_str_has_prefix(text, "http://"))
    prefix_length = 7;
  else if (g_str_has_prefix(text, "https://"))
    prefix_length = 8;
  else if (g_str_has_prefix(text, "mailto:"))
    prefix_length = 7;
  else
    return FALSE;

  int _length = prefix_length;
  int bracket_count = 0;
  while (text[_length] != '\0' && is_valid_url_char(text[_length])) {
    if (text[_length] == '(')
      bracket_count++;
    else if (text[_length] == ')') {
      bracket_count--;
      if (bracket_count < 0)
        break;
    }
    _length++;
  }
  /* Can't end in punctuation */
  while (_length > prefix_length && strchr(",.", text[_length - 1]) != NULL)
    _length--;
  if (_length == prefix_length)
    return FALSE;

  if (length != NULL)
    *length = _length;

  return TRUE;
}

static int find_url(const gchar *text, int *url_length) {
  for (int offset = 0; text[offset] != '\0'; offset++)
    if (is_url(text + offset, url_length))
      return offset;
  return -1;
}

SnapdMarkdownNode *make_url_node(const gchar *text, int length) {
  g_autoptr(GPtrArray) children =
      g_ptr_array_new_with_free_func(g_object_unref);
  g_ptr_array_add(children, make_text_node(text, length));
  return g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                      SNAPD_MARKDOWN_NODE_TYPE_URL, "children", children, NULL);
}

static void extract_urls(GPtrArray *nodes) {
  for (int i = 0; i < nodes->len; i++) {
    SnapdMarkdownNode *node = g_ptr_array_index(nodes, i);
    GPtrArray *children;
    const gchar *text;
    int url_offset, url_length;

    children = snapd_markdown_node_get_children(node);
    if (snapd_markdown_node_get_node_type(node) !=
            SNAPD_MARKDOWN_NODE_TYPE_URL &&
        children != NULL)
      extract_urls(children);

    if (snapd_markdown_node_get_node_type(node) !=
        SNAPD_MARKDOWN_NODE_TYPE_TEXT)
      continue;

    text = snapd_markdown_node_get_text(node);
    url_offset = find_url(text, &url_length);
    if (url_offset >= 0) {
      if (text[url_offset + url_length] != '\0')
        g_ptr_array_insert(nodes, i + 1,
                           make_text_node(text + url_offset + url_length, -1));
      g_ptr_array_insert(nodes, i + 1,
                         make_url_node(text + url_offset, url_length));
      if (url_offset > 0)
        g_ptr_array_insert(nodes, i + 1, make_text_node(text, url_offset));
      g_ptr_array_remove_index(nodes, i);
    }
  }
}

static GPtrArray *markup_inline(SnapdMarkdownParser *self, const gchar *text) {
  /* Split into nodes */
  g_autoptr(GPtrArray) nodes = g_ptr_array_new_with_free_func(g_object_unref);
  g_autoptr(GHashTable) emphasis_info = g_hash_table_new_full(
      g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)emphasis_info_free);
  for (int i = 0; text[i] != '\0';) {
    int start = i;

    /* Extract code spans */
    if (text[start] == '`') {
      int size = backtick_count(text + start);
      int end = start + size;
      while (text[end] != '\0') {
        int s = backtick_count(text + end);
        if (s == size)
          break;
        else if (s == 0)
          end++;
        else
          end += s;
      }
      if (text[end] != '\0') {
        g_autofree gchar *code_text =
            g_strndup(text + start + size, end - start - size);
        g_autofree gchar *stripped_code_text = strip_text(code_text);
        g_ptr_array_add(nodes,
                        make_code_node(SNAPD_MARKDOWN_NODE_TYPE_CODE_SPAN,
                                       stripped_code_text));
        i = end + size;
      } else {
        g_ptr_array_add(nodes,
                        make_paragraph_text_node(self, text + start, size));
        i = start + size;
      }

      continue;
    }

    /* Escaped characters */
    if (text[start] == '\\' && is_punctuation_character(text[start + 1])) {
      g_ptr_array_add(nodes, make_text_node(text + start + 1, 1));
      i = start + 2;
      continue;
    }

    /* Extract emphasis delimiters (we determine later if they are used) */
    if (text[start] == '*' || text[start] == '_') {
      while (text[i] == text[start])
        i++;

      gboolean is_left_flanking = is_left_flanking_delimiter_run(text, start);
      gboolean is_right_flanking = is_right_flanking_delimiter_run(text, start);

      g_autoptr(EmphasisInfo) info = emphasis_info_new();
      info->character = text[start];
      info->length = i - start;
      if (text[start] == '_') {
        info->can_open_emphasis =
            is_left_flanking &&
            (!is_right_flanking ||
             (start > 0 && is_punctuation_character(text[start - 1])));
        info->can_close_emphasis =
            is_right_flanking &&
            (!is_left_flanking || is_punctuation_character(text[i]));
      } else if (text[start] == '*') {
        info->can_open_emphasis = is_left_flanking;
        info->can_close_emphasis = is_right_flanking;
      }
      SnapdMarkdownNode *node =
          make_paragraph_text_node(self, text + start, i - start);
      g_ptr_array_add(nodes, node);
      g_hash_table_insert(emphasis_info, node, g_steal_pointer(&info));
      continue;
    }

    /* Extract text until next potential emphasis or code span */
    while (text[i] != '\0') {
      if (text[i] == '*' || text[i] == '_' || text[i] == '`')
        break;

      if (text[i] == '\\' && is_punctuation_character(text[i + 1]))
        break;

      i++;
    }
    g_ptr_array_add(nodes,
                    make_paragraph_text_node(self, text + start, i - start));
  }

  /* Convert nodes into emphasis */
  find_emphasis(nodes, emphasis_info);

  /* Combine sequential text nodes */
  combine_text_nodes(nodes);

  /* Extract URLs */
  extract_urls(nodes);

  return g_steal_pointer(&nodes);
}

static GPtrArray *markdown_to_markup(SnapdMarkdownParser *self,
                                     const gchar *text) {
  /* Snap supports the following subset of CommonMark (https://commonmark.org/):
   *
   * Indented code blocks
   * Paragraphs
   * Blank lines
   * Lists
   * Backslash escapes
   * Code spans
   * Emphasis and strong emphasis
   * Textual content
   *
   * In addition, links are automatically converted to hyperlinks.
   */

  /* 1. Split into lines */
  g_autoptr(GPtrArray) line_array = g_ptr_array_new_with_free_func(g_free);
  int line_start = 0;
  for (int i = 0; text[i] != '\0'; i++) {
    if (text[i] == '\n' || text[i] == '\r') {
      if (text[i] == '\r' && text[i + 1] == '\n')
        i++;
      g_ptr_array_add(line_array,
                      g_strndup(text + line_start, i - line_start + 1));
      line_start = i + 1;
    }
  }
  if (text[line_start] != '\0')
    g_ptr_array_add(line_array, g_strdup(text + line_start));
  g_ptr_array_add(line_array, NULL);
  GStrv lines = (GStrv)line_array->pdata;

  /* 2. Split lines into blocks (paragraphs, lists, code) */
  int line_number = 0;
  g_autoptr(GPtrArray) nodes = g_ptr_array_new_with_free_func(g_object_unref);
  while (lines[line_number] != NULL) {
    /* Skip empty lines */
    if (parse_empty_line(lines[line_number])) {
      line_number++;
      continue;
    }

    g_autofree gchar *block_text = NULL;
    int bullet_offset = 0;
    gchar bullet_symbol;
    g_autofree gchar *bullet_text = NULL;
    /* Indented code blocks */
    if (parse_indented_code_block(lines[line_number], &block_text)) {
      g_autoptr(GString) code_text = g_string_new("");
      g_string_append(code_text, block_text);

      while (TRUE) {
        line_number++;

        if (lines[line_number] == NULL)
          break;

        g_autofree gchar *text = NULL;
        if (parse_indented_code_block(lines[line_number], &text)) {
          g_string_append(code_text, text);
        } else if (parse_empty_line(lines[line_number]))
          g_string_append_c(code_text, '\n');
        else
          break;
      }

      /* Remove trailing empty lines */
      while (g_str_has_suffix(code_text->str, "\n\n"))
        g_string_truncate(code_text, code_text->len - 1);

      g_ptr_array_add(nodes, make_code_node(SNAPD_MARKDOWN_NODE_TYPE_CODE_BLOCK,
                                            code_text->str));
    }
    /* Bullet lists */
    else if (parse_bullet_list_item(lines[line_number], &bullet_offset,
                                    &bullet_symbol, &bullet_text)) {
      g_autoptr(GPtrArray) list_items =
          g_ptr_array_new_with_free_func(g_object_unref);
      g_autoptr(GString) list_data = g_string_new(bullet_text);
      gboolean starts_with_empty_line = bullet_text[0] == '\0';
      gboolean have_item = TRUE;
      while (TRUE) {
        line_number++;
        if (lines[line_number] == NULL)
          break;

        if (parse_empty_line(lines[line_number])) {
          if (starts_with_empty_line)
            break;
          g_string_append(list_data, lines[line_number]);
          have_item = TRUE;
          continue;
        }
        starts_with_empty_line = FALSE;
        g_autofree gchar *line_text = NULL;
        if (parse_list_item_line(lines[line_number], bullet_offset,
                                 &line_text)) {
          g_string_append(list_data, line_text);
          have_item = TRUE;
          continue;
        }

        if (have_item) {
          g_autoptr(GPtrArray) children =
              markdown_to_markup(self, list_data->str);
          g_ptr_array_add(list_items,
                          g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                                       SNAPD_MARKDOWN_NODE_TYPE_LIST_ITEM,
                                       "children", children, NULL));
          g_string_assign(list_data, "");
          have_item = FALSE;
        }

        // FIXME: Check matching offset
        g_autofree gchar *text = NULL;
        gchar symbol;
        if (!parse_bullet_list_item(lines[line_number], &bullet_offset, &symbol,
                                    &text))
          break;
        if (symbol != bullet_symbol)
          break;
        g_string_assign(list_data, text);
        have_item = TRUE;
      }

      if (have_item) {
        g_autoptr(GPtrArray) children =
            markdown_to_markup(self, list_data->str);
        g_ptr_array_add(list_items,
                        g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                                     SNAPD_MARKDOWN_NODE_TYPE_LIST_ITEM,
                                     "children", children, NULL));
      }

      g_ptr_array_add(nodes,
                      g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                                   SNAPD_MARKDOWN_NODE_TYPE_UNORDERED_LIST,
                                   "children", list_items, NULL));
    }
    /* Paragraphs */
    else {
      g_autoptr(GString) paragraph_text = g_string_new("");
      while (TRUE) {
        g_autofree gchar *text = NULL;
        parse_paragraph(lines[line_number], &text);

        g_string_append(paragraph_text, text);

        line_number++;

        /* Out of data */
        if (lines[line_number] == NULL)
          break;

        /* Break on empty line */
        if (parse_empty_line(lines[line_number]))
          break;

        /* Break on non-empty list items */
        g_autofree gchar *bullet_text = NULL;
        if (parse_bullet_list_item(lines[line_number], NULL, NULL,
                                   &bullet_text)) {
          if (bullet_text[0] != '\0')
            break;
        }
      }

      /* Strip leading and trailing whitespace */
      int offset = 0;
      while (isspace(paragraph_text->str[offset]))
        offset++;
      int length = paragraph_text->len;
      while (length > 0 && isspace(paragraph_text->str[length - 1]))
        length--;
      if (offset + length > paragraph_text->len)
        length = 0;
      g_autofree gchar *stripped_text =
          g_strndup(paragraph_text->str + offset, length);

      g_autoptr(GPtrArray) children = markup_inline(self, stripped_text);
      g_ptr_array_add(nodes, g_object_new(SNAPD_TYPE_MARKDOWN_NODE, "node-type",
                                          SNAPD_MARKDOWN_NODE_TYPE_PARAGRAPH,
                                          "children", children, NULL));
    }
  }

  return g_steal_pointer(&nodes);
}

/**
 * snapd_markdown_parser_new:
 * @version: version supported by the client.
 *
 * Create an object to parse markdown text.
 *
 * Returns: a new #SnapdMarkdownParser
 *
 * Since: 1.48
 */
SnapdMarkdownParser *snapd_markdown_parser_new(SnapdMarkdownVersion version) {
  return g_object_new(SNAPD_TYPE_MARKDOWN_PARSER, NULL);
}

/**
 * snapd_markdown_parser_set_preserve_whitespace:
 * @parser: a #SnapdMarkdownParser.
 * @preserve_whitespace: %TRUE if the parse should keep paragraph whitespace
 * intact.
 *
 * Consecutive paragraph whitespace (space, tabs, newlines) is automatically
 * combined into a single space character. This renders the paragraphs in the
 * form that HTML uses. If you need the original whitespace that the markdown
 * author wrote then set this to %FALSE.
 *
 * Since: 1.48
 */
void snapd_markdown_parser_set_preserve_whitespace(
    SnapdMarkdownParser *self, gboolean preserve_whitespace) {
  g_return_if_fail(SNAPD_IS_MARKDOWN_PARSER(self));
  self->preserve_whitespace = preserve_whitespace;
}

/**
 * snapd_markdown_parser_get_preserve_whitespace:
 * @parser: a #SnapdMarkdownParser.
 *
 * Check if paragraph whitespace will be kept intact.
 *
 * Returns: %TRUE if paragraph whitespace is preserved.
 *
 * Since: 1.48
 */
gboolean
snapd_markdown_parser_get_preserve_whitespace(SnapdMarkdownParser *self) {
  g_return_val_if_fail(SNAPD_IS_MARKDOWN_PARSER(self), FALSE);
  return self->preserve_whitespace;
}

/**
 * snapd_markdown_parser_parse:
 * @parser: a #SnapdMarkdownParser.
 * @text: text to parse.
 *
 * Convert text in snapd markdown format to markup.
 *
 * Returns: (transfer container) (element-type SnapdMarkdownNode): Text split
 * into blocks.
 *
 * Since: 1.48
 */
GPtrArray *snapd_markdown_parser_parse(SnapdMarkdownParser *self,
                                       const gchar *text) {
  g_return_val_if_fail(SNAPD_IS_MARKDOWN_PARSER(self), NULL);
  g_return_val_if_fail(text != NULL, NULL);
  return markdown_to_markup(self, text);
}

static void snapd_markdown_parser_class_init(SnapdMarkdownParserClass *klass) {}

static void snapd_markdown_parser_init(SnapdMarkdownParser *self) {}
