/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2018-2024 Matthias Klumpp
* Copyright (C) 2014-2016 Richard Hughes
*
* Licensed under the GNU Lesser General Public License Version 2.1
*
* 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.1 of the license, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*/
/**
* SECTION:as-news-convert
* @short_description: Read and write NEWS/Changelog files from metainfo
* @include: appstream.h
*
* Read NEWS and other types of release information files and convert them
* to AppStream metainfo data.
* Also, write NEWS files from #AsRelease release information.
*
* These functions are private/internal.
*/
#include "config.h"
#include "as-news-convert.h"
#include "as-metadata.h"
#include "as-xml.h"
#include "as-yaml.h"
#include "as-utils-private.h"
#include "as-release-private.h"
/**
* as_news_format_kind_to_string:
* @kind: the #AsNewsFormatKind.
*
* Converts the enumerated value to an text representation.
*
* Returns: string version of @kind
*
* Since: 0.12.9
**/
const gchar *
as_news_format_kind_to_string (AsNewsFormatKind kind)
{
if (kind == AS_NEWS_FORMAT_KIND_YAML)
return "yaml";
if (kind == AS_NEWS_FORMAT_KIND_TEXT)
return "text";
if (kind == AS_NEWS_FORMAT_KIND_MARKDOWN)
return "markdown";
return "unknown";
}
/**
* as_news_format_kind_from_string:
* @kind_str: the string.
*
* Converts the text representation to an enumerated value.
*
* Returns: a #AsNewsFormatKind or %AS_NEWS_FORMAT_KIND_UNKNOWN for unknown
*
* Since: 0.12.9
**/
AsNewsFormatKind
as_news_format_kind_from_string (const gchar *kind_str)
{
if (kind_str == NULL)
return AS_NEWS_FORMAT_KIND_UNKNOWN;
if (g_strcmp0 (kind_str, "yaml") == 0)
return AS_NEWS_FORMAT_KIND_YAML;
if (g_strcmp0 (kind_str, "text") == 0)
return AS_NEWS_FORMAT_KIND_TEXT;
if (g_strcmp0 (kind_str, "markdown") == 0)
return AS_NEWS_FORMAT_KIND_MARKDOWN;
return AS_NEWS_FORMAT_KIND_UNKNOWN;
}
/**
* as_releases_to_metainfo_xml_chunk:
*
* Internal helper method to convert release objects to XML.
*/
gchar *
as_releases_to_metainfo_xml_chunk (GPtrArray *releases, GError **error)
{
g_autoptr(AsContext) ctx = NULL;
xmlNode *root;
xmlNode *n_releases;
g_auto(GStrv) strv = NULL;
g_autofree gchar *xml_raw = NULL;
guint lines;
ctx = as_context_new ();
as_context_set_locale (ctx, "C");
as_context_set_style (ctx, AS_FORMAT_STYLE_METAINFO);
root = xmlNewNode (NULL, (xmlChar *) "component");
n_releases = as_xml_add_node (root, "releases");
for (guint i = 0; i < releases->len; ++i) {
AsRelease *release = AS_RELEASE (g_ptr_array_index (releases, i));
as_release_to_xml_node (release, ctx, n_releases);
}
xml_raw = as_xml_node_free_to_str (root, error);
if ((error != NULL) && (*error != NULL))
return NULL;
/* this is inefficient, but we don't actually need to be very fast here */
strv = g_strsplit (xml_raw, "\n", -1);
lines = g_strv_length (strv);
if (lines < 4)
return NULL; /* something went wrong here */
g_free (strv[lines - 1]);
g_free (strv[lines - 2]);
strv[lines - 2] = NULL;
return g_strjoinv ("\n", strv + 2);
}
/**
* as_news_yaml_to_release:
*/
static GPtrArray *
as_news_yaml_to_releases (const gchar *yaml_data, gint limit, GError **error)
{
yaml_parser_t parser;
yaml_event_t event;
gboolean parse = TRUE;
gboolean ret = TRUE;
g_autoptr(GPtrArray) releases = NULL;
if (yaml_data == NULL) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"YAML news file was empty.");
return NULL;
}
releases = g_ptr_array_new_with_free_func (g_object_unref);
/* initialize YAML parser */
yaml_parser_initialize (&parser);
yaml_parser_set_input_string (&parser, (unsigned char *) yaml_data, strlen (yaml_data));
while (parse) {
if (!yaml_parser_parse (&parser, &event)) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_PARSE,
"Could not parse YAML: %s",
parser.problem);
ret = FALSE;
break;
}
if (event.type == YAML_DOCUMENT_START_EVENT) {
GNode *n;
GError *tmp_error = NULL;
g_autoptr(GNode) root = NULL;
g_autoptr(AsRelease) rel = as_release_new ();
root = g_node_new (g_strdup (""));
as_yaml_parse_layer (&parser, root, &tmp_error);
if (tmp_error != NULL) {
/* stop immediately, since we found an error when parsing the document */
g_propagate_error (error, tmp_error);
g_free (root->data);
yaml_event_delete (&event);
ret = FALSE;
parse = FALSE;
break;
}
for (n = root->children; n != NULL; n = n->next) {
const gchar *key;
const gchar *value;
if ((n->data == NULL) || (n->children == NULL)) {
/* skip an empty node */
continue;
}
key = as_yaml_node_get_key (n);
value = as_yaml_node_get_value (n);
if (g_strcmp0 (key, "Version") == 0) {
as_release_set_version (rel, value);
} else if (g_strcmp0 (key, "Date") == 0) {
as_release_set_date (rel, value);
} else if (g_strcmp0 (key, "Type") == 0) {
AsReleaseKind rkind = as_release_kind_from_string (value);
if (rkind != AS_RELEASE_KIND_UNKNOWN)
as_release_set_kind (rel, rkind);
} else if ((g_strcmp0 (key, "Description") == 0) ||
(g_strcmp0 (key, "Notes") == 0)) {
g_autoptr(GString) str = g_string_new ("");
if ((n->children != NULL) && (n->children->next != NULL)) {
GNode *dn;
g_string_append (str, "");
for (dn = n->children; dn != NULL; dn = dn->next) {
g_autofree gchar
*escaped = g_markup_escape_text (
as_yaml_node_get_key (dn),
-1);
g_string_append_printf (str,
"- %s
",
escaped);
}
g_string_append (str, "
");
} else {
/* we only have one list entry, or no list at all and a freeform text instead. Convert to paragraphs */
g_auto(GStrv)
paras = g_strsplit (value, "\n\n", -1);
for (guint i = 0; paras[i] != NULL; i++) {
g_auto(GStrv) lines = NULL;
gboolean in_listing = FALSE;
gboolean in_paragraph = FALSE;
g_autofree gchar *escaped =
g_markup_escape_text (paras[i], -1);
lines = g_strsplit (escaped, "\n", -1);
for (guint j = 0; lines[j] != NULL; j++) {
if (g_str_has_prefix (lines[j],
" -") ||
g_str_has_prefix (lines[j],
" *")) {
/* we have a list */
if (in_paragraph) {
g_string_truncate (
str,
str->len - 1);
g_string_append (
str,
"
\n");
in_paragraph =
FALSE;
}
if (in_listing) {
g_string_append (
str,
"\n");
} else {
g_string_append (
str,
"\n- ");
}
g_string_append (str,
lines[j] +
3);
in_listing = TRUE;
continue;
} else if (in_listing) {
if (g_str_has_prefix (
lines[j],
" ")) {
g_string_append_printf (
str,
" %s",
lines[j] + 3);
} else {
g_string_append (
str,
"
\n"
"ul>\n");
in_listing = FALSE;
g_string_append_printf (
str,
"%s\n",
lines[j]);
in_paragraph = TRUE;
}
} else {
g_string_append_printf (
str,
"
%s\n",
lines[j]);
in_paragraph = TRUE;
}
}
if (in_listing)
g_string_append (str,
"
\n\n");
if (in_paragraph) {
g_string_truncate (str,
str->len - 1);
g_string_append (str, "\n");
}
}
}
/* FIXME: Silences an invalid null-dereference warning in GCC 13 which happens when
* GString is used in g_autoptr() */
g_assert (str != NULL);
as_release_set_description (rel, str->str, "C");
}
}
if (as_release_get_version (rel) != NULL) {
g_ptr_array_add (releases, g_steal_pointer (&rel));
if (limit > 0 && releases->len >= (guint) limit)
parse = FALSE;
}
g_node_traverse (root,
G_IN_ORDER,
G_TRAVERSE_ALL,
-1,
as_yaml_free_node,
NULL);
}
/* stop if end of stream is reached */
if (event.type == YAML_STREAM_END_EVENT)
parse = FALSE;
yaml_event_delete (&event);
}
yaml_parser_delete (&parser);
/* return NULL on error, otherwise return the list of releases */
if (ret)
return g_steal_pointer (&releases);
else
return NULL;
}
/**
* as_news_yaml_write_handler_cb:
*
* Helper function to store the emitted YAML document.
*/
static int
as_news_yaml_write_handler_cb (void *ptr, unsigned char *buffer, size_t size)
{
GString *str;
str = (GString *) ptr;
g_string_append_len (str, (const gchar *) buffer, size);
return 1;
}
/**
* as_news_releases_to_yaml:
*/
static gboolean
as_news_releases_to_yaml (GPtrArray *releases, gchar **yaml_data)
{
yaml_emitter_t emitter;
yaml_event_t event;
gboolean res = FALSE;
gboolean report_validation_passed = TRUE;
GString *yaml_result = g_string_new ("");
yaml_emitter_initialize (&emitter);
yaml_emitter_set_indent (&emitter, 2);
yaml_emitter_set_unicode (&emitter, TRUE);
yaml_emitter_set_width (&emitter, 255);
yaml_emitter_set_output (&emitter, as_news_yaml_write_handler_cb, yaml_result);
/* emit start event */
yaml_stream_start_event_initialize (&event, YAML_UTF8_ENCODING);
if (!yaml_emitter_emit (&emitter, &event)) {
g_critical ("Failed to initialize YAML emitter.");
g_string_free (yaml_result, TRUE);
yaml_emitter_delete (&emitter);
return FALSE;
}
for (guint i = 0; i < releases->len; i++) {
AsRelease *rel = AS_RELEASE (g_ptr_array_index (releases, i));
AsReleaseKind rkind = as_release_get_kind (rel);
g_autoptr(AsContext) rel_context = NULL;
const gchar *rel_active_locale = NULL;
const gchar *desc_markup;
rel_context = as_release_get_context (rel);
if (rel_context == NULL) {
rel_context = as_context_new ();
as_release_set_context (rel, rel_context);
} else {
rel_context = g_object_ref (rel_context);
}
rel_active_locale = as_context_get_locale (rel_context);
/* we only write the untranslated strings */
as_context_set_locale (rel_context, "C");
desc_markup = as_release_get_description (rel);
/* new document for this release */
yaml_document_start_event_initialize (&event, NULL, NULL, NULL, FALSE);
res = yaml_emitter_emit (&emitter, &event);
g_assert (res);
/* main dict start */
as_yaml_mapping_start (&emitter);
as_yaml_emit_scalar_raw (&emitter, "Version");
as_yaml_emit_scalar_raw (&emitter, as_release_get_version (rel));
as_yaml_emit_entry (&emitter, "Date", as_release_get_date (rel));
if (rkind != AS_RELEASE_KIND_STABLE)
as_yaml_emit_entry (&emitter, "Type", as_release_kind_to_string (rkind));
if (desc_markup != NULL) {
if (g_strstr_len (desc_markup, -1, "") != NULL) {
/* we have paragraphs - just convert the markup to a simple text */
g_autofree gchar *md = NULL;
md = as_markup_convert (desc_markup, AS_MARKUP_KIND_MARKDOWN, NULL);
if (md != NULL)
as_yaml_emit_long_entry_literal (&emitter,
"Description",
md);
} else {
xmlDoc *doc = NULL;
xmlNode *root;
xmlNode *iter;
g_autofree gchar *xmldata = NULL;
/* make XML parser happy by providing a root element */
xmldata = g_strdup_printf ("%s", desc_markup);
doc = xmlParseDoc ((xmlChar *) xmldata);
if (doc == NULL)
goto xml_end;
root = xmlDocGetRootElement (doc);
if (root == NULL) {
/* document was empty */
goto xml_end;
}
as_yaml_emit_scalar (&emitter, "Description");
as_yaml_sequence_start (&emitter);
for (iter = root->children; iter != NULL; iter = iter->next) {
xmlNode *iter2;
/* discard spaces */
if (iter->type != XML_ELEMENT_NODE)
continue;
if ((g_strcmp0 ((gchar *) iter->name, "ul") == 0) ||
(g_strcmp0 ((gchar *) iter->name, "ol") == 0)) {
/* iterate over itemize contents */
for (iter2 = iter->children; iter2 != NULL;
iter2 = iter2->next) {
if (iter2->type != XML_ELEMENT_NODE)
continue;
if (g_strcmp0 ((gchar *) iter2->name,
"li") == 0) {
g_autofree gchar *content =
as_xml_get_node_value_raw (
iter2);
as_yaml_emit_scalar (
&emitter,
as_strstripnl (content));
}
}
}
}
as_yaml_sequence_end (&emitter);
xml_end:
if (doc != NULL)
xmlFreeDoc (doc);
}
}
as_context_set_locale (rel_context, rel_active_locale);
/* main dict end */
as_yaml_mapping_end (&emitter);
/* finalize the document */
yaml_document_end_event_initialize (&event, 1);
res = yaml_emitter_emit (&emitter, &event);
g_assert (res);
}
/* end stream */
yaml_stream_end_event_initialize (&event);
res = yaml_emitter_emit (&emitter, &event);
g_assert (res);
yaml_emitter_flush (&emitter);
yaml_emitter_delete (&emitter);
*yaml_data = g_string_free (yaml_result, FALSE);
return report_validation_passed;
}
typedef enum {
AS_NEWS_SECTION_KIND_UNKNOWN,
AS_NEWS_SECTION_KIND_HEADER,
AS_NEWS_SECTION_KIND_NOTES,
AS_NEWS_SECTION_KIND_BUGFIX,
AS_NEWS_SECTION_KIND_FEATURES,
AS_NEWS_SECTION_KIND_MISC,
AS_NEWS_SECTION_KIND_TRANSLATION,
AS_NEWS_SECTION_KIND_DOCUMENTATION,
AS_NEWS_SECTION_KIND_CONTRIBUTORS,
AS_NEWS_SECTION_KIND_TRANSLATORS,
AS_NEWS_SECTION_KIND_LAST
} AsNewsSectionKind;
static AsNewsSectionKind
as_news_text_guess_section (const gchar *lines)
{
if (g_strstr_len (lines, -1, "~~~~") != NULL)
return AS_NEWS_SECTION_KIND_HEADER;
if (g_strstr_len (lines, -1, "----") != NULL)
return AS_NEWS_SECTION_KIND_HEADER;
if (g_strstr_len (lines, -1, "Bugfix:\n") != NULL)
return AS_NEWS_SECTION_KIND_BUGFIX;
if (g_strstr_len (lines, -1, "Bugfixes:\n") != NULL)
return AS_NEWS_SECTION_KIND_BUGFIX;
if (g_strstr_len (lines, -1, "Bug fixes:\n") != NULL)
return AS_NEWS_SECTION_KIND_BUGFIX;
if (g_strstr_len (lines, -1, "Features:\n") != NULL)
return AS_NEWS_SECTION_KIND_FEATURES;
if (g_strstr_len (lines, -1, "Removed features:\n") != NULL)
return AS_NEWS_SECTION_KIND_FEATURES;
if (g_strstr_len (lines, -1, "Specification:\n") != NULL)
return AS_NEWS_SECTION_KIND_DOCUMENTATION;
if (g_strstr_len (lines, -1, "Documentation:\n") != NULL)
return AS_NEWS_SECTION_KIND_DOCUMENTATION;
if (g_strstr_len (lines, -1, "Notes:\n") != NULL)
return AS_NEWS_SECTION_KIND_NOTES;
if (g_strstr_len (lines, -1, "Note:\n") != NULL)
return AS_NEWS_SECTION_KIND_NOTES;
if (g_strstr_len (lines, -1, "Miscellaneous:\n") != NULL)
return AS_NEWS_SECTION_KIND_MISC;
if (g_strstr_len (lines, -1, "Misc:\n") != NULL)
return AS_NEWS_SECTION_KIND_MISC;
if (g_strstr_len (lines, -1, "Translations:\n") != NULL)
return AS_NEWS_SECTION_KIND_TRANSLATION;
if (g_strstr_len (lines, -1, "Translation:\n") != NULL)
return AS_NEWS_SECTION_KIND_TRANSLATION;
if (g_strstr_len (lines, -1, "Translations\n") != NULL)
return AS_NEWS_SECTION_KIND_TRANSLATION;
if (g_strstr_len (lines, -1, "Contributors:\n") != NULL)
return AS_NEWS_SECTION_KIND_CONTRIBUTORS;
if (g_strstr_len (lines, -1, "With contributions from:\n") != NULL)
return AS_NEWS_SECTION_KIND_CONTRIBUTORS;
if (g_strstr_len (lines, -1, "Thanks to:\n") != NULL)
return AS_NEWS_SECTION_KIND_CONTRIBUTORS;
if (g_strstr_len (lines, -1, "Translators:\n") != NULL)
return AS_NEWS_SECTION_KIND_TRANSLATORS;
return AS_NEWS_SECTION_KIND_UNKNOWN;
}
static void
as_news_text_add_markup (GString *desc, const gchar *tag, const gchar *line)
{
/* empty line means do nothing */
if (line != NULL && line[0] == '\0')
return;
if (line == NULL) {
g_string_append_printf (desc, "<%s>\n", tag);
} else {
g_autofree gchar *escaped = g_markup_escape_text (line, -1);
g_string_append_printf (desc, "<%s>%s%s>\n", tag, escaped, tag);
}
}
static gboolean
as_news_text_to_release_hdr (AsRelease *release, GString *desc, const gchar *txt, GError **error)
{
guint i;
const gchar *version = NULL;
const gchar *release_txt = NULL;
g_auto(GStrv) release_split = NULL;
g_autoptr(GDateTime) dt = NULL;
g_auto(GStrv) lines = NULL;
g_autofree gchar *date_str = NULL;
/* get info */
lines = g_strsplit (txt, "\n", -1);
for (i = 0; lines[i] != NULL; i++) {
if (g_str_has_prefix (lines[i], "Version ")) {
version = lines[i] + 8;
continue;
}
if (g_str_has_prefix (lines[i], "Released: ")) {
release_txt = lines[i] + 10;
continue;
}
}
/* check these exist */
if (version == NULL) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to find version in: %s",
txt);
return FALSE;
}
if (release_txt == NULL) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to find release in: %s",
txt);
return FALSE;
}
/* apply version number */
as_release_set_version (release, version);
/* check if the release is unreleased */
if ((g_strstr_len (release_txt, -1, "-xx") != NULL) ||
(g_strstr_len (release_txt, -1, "-XX") != NULL) ||
(g_strstr_len (release_txt, -1, "-??") != NULL)) {
g_autoptr(GDateTime) dt_now = g_date_time_new_now_local ();
date_str = g_date_time_format_iso8601 (dt_now);
as_release_set_kind (release, AS_RELEASE_KIND_DEVELOPMENT);
as_release_set_date (release, date_str);
/* no further date parsing is needed at this point */
return TRUE;
} else {
as_release_set_kind (release, AS_RELEASE_KIND_STABLE);
}
/* parse date */
release_split = g_strsplit (release_txt, "-", -1);
if (g_strv_length (release_split) != 3) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to parse release: %s",
release_txt);
return FALSE;
}
dt = g_date_time_new_local (atoi (release_split[0]),
atoi (release_split[1]),
atoi (release_split[2]),
0,
0,
0);
if (dt == NULL) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to create release: %s",
release_txt);
return FALSE;
}
/* set release properties */
date_str = g_strdup_printf ("%s-%s-%s",
release_split[0],
release_split[1],
release_split[2]);
as_release_set_date (release, date_str);
return TRUE;
}
static gboolean
as_news_text_to_list_markup (GString *desc, gchar **lines, GError **error)
{
guint i;
as_news_text_add_markup (desc, "ul", NULL);
for (i = 0; lines[i] != NULL; i++) {
guint prefix = 0;
g_strstrip (lines[i]);
if ((g_str_has_prefix (lines[i], "- ")) || (g_str_has_prefix (lines[i], "* ")))
prefix = 2;
as_news_text_add_markup (desc, "li", lines[i] + prefix);
}
as_news_text_add_markup (desc, "/ul", NULL);
return TRUE;
}
static gboolean
as_news_text_to_para_markup (GString *desc, const gchar *txt, GError **error)
{
g_auto(GStrv) lines = NULL;
gboolean para_generated = FALSE;
if (g_strstr_len (txt, -1, "* ") != NULL || g_strstr_len (txt, -1, "- ") != NULL) {
/* enumerations to paragraphs */
lines = g_strsplit (txt, "\n", -1);
for (guint i = 1; lines[i] != NULL; i++) {
guint prefix = 0;
g_strstrip (lines[i]);
if ((g_str_has_prefix (lines[i], "- ")) ||
(g_str_has_prefix (lines[i], "* ")))
prefix = 2;
as_news_text_add_markup (desc, "p", lines[i] + prefix);
para_generated = TRUE;
}
} else {
/* freeform text to paragraphs */
const gchar *txt_content = g_strstr_len (txt, -1, "\n");
if (txt_content == NULL) {
g_set_error (
error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to write sensible paragraph markup (missing header) for: %s.",
txt);
return FALSE;
}
lines = g_strsplit (txt_content, "\n\n", -1);
for (guint i = 0; lines[i] != NULL; i++) {
g_strstrip (lines[i]);
as_news_text_add_markup (desc, "p", lines[i]);
para_generated = TRUE;
}
}
if (!para_generated) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to write sensible paragraph markup (source data may be "
"malformed) for: %s.",
txt);
return FALSE;
}
return TRUE;
}
/**
* as_news_text_to_releases:
*/
static GPtrArray *
as_news_text_to_releases (const gchar *data, gint limit, GError **error)
{
guint i;
g_autoptr(GString) data_str = NULL;
g_autoptr(GString) desc = NULL;
g_auto(GStrv) split = NULL;
g_autoptr(GPtrArray) releases = NULL;
g_autoptr(AsRelease) rel = NULL;
gboolean limit_reached = FALSE;
if (data == NULL) {
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"YAML news file was empty.");
return NULL;
}
releases = g_ptr_array_new_with_free_func (g_object_unref);
/* try to unsplit lines */
data_str = g_string_new (data);
as_gstring_replace (data_str, "\n ", " ", 0);
/* break up into sections */
desc = g_string_new ("");
split = g_strsplit (data_str->str, "\n\n", -1);
for (i = 0; split[i] != NULL; i++) {
g_auto(GStrv) lines = NULL;
/* ignore empty sections */
if (split[i][0] == '\0')
continue;
switch (as_news_text_guess_section (split[i])) {
case AS_NEWS_SECTION_KIND_HEADER: {
/* flush old release content and create new release */
if (desc->len > 0) {
as_release_set_description (rel, desc->str, "C");
g_string_truncate (desc, 0);
}
if (rel != NULL) {
g_ptr_array_add (releases, g_steal_pointer (&rel));
if (limit > 0 && releases->len >= (guint) limit)
limit_reached = TRUE;
}
rel = as_release_new ();
/* parse header */
if (!as_news_text_to_release_hdr (rel, desc, split[i], error)) {
g_prefix_error (error,
"Unable to parse NEWS header '%s': ",
split[i]);
return NULL;
}
break;
}
case AS_NEWS_SECTION_KIND_BUGFIX:
lines = g_strsplit (split[i], "\n", -1);
if (g_strv_length (lines) == 2) {
as_news_text_add_markup (desc,
"p",
"This release fixes the following bug:");
} else {
as_news_text_add_markup (desc,
"p",
"This release fixes the following bugs:");
}
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
break;
case AS_NEWS_SECTION_KIND_NOTES:
if (!as_news_text_to_para_markup (desc, split[i], error))
return FALSE;
break;
case AS_NEWS_SECTION_KIND_FEATURES:
lines = g_strsplit (split[i], "\n", -1);
if (g_strv_length (lines) == 2) {
as_news_text_add_markup (
desc,
"p",
"This release adds the following feature:");
} else {
as_news_text_add_markup (
desc,
"p",
"This release adds the following features:");
}
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
break;
case AS_NEWS_SECTION_KIND_MISC:
lines = g_strsplit (split[i], "\n", -1);
if (g_strv_length (lines) == 2) {
as_news_text_add_markup (
desc,
"p",
"This release includes the following change:");
} else {
as_news_text_add_markup (
desc,
"p",
"This release includes the following changes:");
}
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
break;
case AS_NEWS_SECTION_KIND_DOCUMENTATION:
lines = g_strsplit (split[i], "\n", -1);
as_news_text_add_markup (desc, "p", "This release updates documentation:");
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
break;
case AS_NEWS_SECTION_KIND_TRANSLATION:
as_news_text_add_markup (desc, "p", "This release updates translations.");
break;
case AS_NEWS_SECTION_KIND_CONTRIBUTORS:
as_news_text_add_markup (desc, "p", "With contributions from:");
if (g_strstr_len (split[i], -1, "* ") != NULL ||
g_strstr_len (split[i], -1, "- ") != NULL) {
lines = g_strsplit (split[i], "\n", -1);
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
} else {
if (!as_news_text_to_para_markup (desc, split[i], error))
return FALSE;
}
break;
case AS_NEWS_SECTION_KIND_TRANSLATORS:
as_news_text_add_markup (desc, "p", "Updated localization by:");
if (g_strstr_len (split[i], -1, "* ") != NULL ||
g_strstr_len (split[i], -1, "- ") != NULL) {
lines = g_strsplit (split[i], "\n", -1);
if (!as_news_text_to_list_markup (desc, lines + 1, error))
return FALSE;
} else {
if (!as_news_text_to_para_markup (desc, split[i], error))
return FALSE;
}
break;
default:
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Failed to detect section '%s'",
split[i]);
return FALSE;
}
if (limit_reached)
break;
}
/* flush old release content */
if (desc->len > 0 && !limit_reached) {
as_release_set_description (rel, desc->str, "C");
g_ptr_array_add (releases, g_steal_pointer (&rel));
}
return g_steal_pointer (&releases);
}
/**
* as_news_releases_to_text:
*/
static gboolean
as_news_releases_to_text (GPtrArray *releases, gchar **md_data, AsNewsFormatKind kind)
{
const gchar *header_line_char;
g_autoptr(GString) str = NULL;
if (kind == AS_NEWS_FORMAT_KIND_MARKDOWN)
header_line_char = "-";
else
header_line_char = "~";
str = g_string_new ("");
for (guint i = 0; i < releases->len; i++) {
const gchar *tmp;
g_autofree gchar *version = NULL;
g_autofree gchar *date = NULL;
g_autoptr(GDateTime) dt = NULL;
AsRelease *rel = AS_RELEASE (g_ptr_array_index (releases, i));
/* write version with underline */
version = g_strdup_printf ("Version %s", as_release_get_version (rel));
g_string_append_printf (str, "%s\n", version);
for (guint j = 0; version[j] != '\0'; j++)
g_string_append (str, header_line_char);
g_string_append (str, "\n");
/* write release */
if (as_release_get_timestamp (rel) > 0) {
dt = g_date_time_new_from_unix_utc (
(gint64) as_release_get_timestamp (rel));
date = g_date_time_format (dt, "%F");
g_string_append_printf (str, "Released: %s\n\n", date);
}
/* transform description */
tmp = as_release_get_description (rel);
if (tmp != NULL) {
g_autofree gchar *md = NULL;
md = as_markup_convert (tmp, AS_MARKUP_KIND_MARKDOWN, NULL);
if (md == NULL)
return FALSE;
g_string_append_printf (str, "%s\n", md);
}
g_string_append (str, "\n");
}
if (str->len > 1)
g_string_truncate (str, str->len - 1);
*md_data = g_string_free (str, FALSE);
str = NULL;
return TRUE;
}
/**
* as_news_to_releases_from_data:
*
* Convert NEWS data to a list of AsRelease elements.
*/
GPtrArray *
as_news_to_releases_from_data (const gchar *data,
AsNewsFormatKind kind,
gint entry_limit,
gint translatable_limit,
GError **error)
{
GPtrArray *releases = NULL;
if (kind == AS_NEWS_FORMAT_KIND_YAML)
releases = as_news_yaml_to_releases (data, entry_limit, error);
if (kind == AS_NEWS_FORMAT_KIND_TEXT || kind == AS_NEWS_FORMAT_KIND_MARKDOWN)
releases = as_news_text_to_releases (data, entry_limit, error);
if (releases == NULL) {
/* if no error was set, we simply had no idea about the format.
* Otherwise, parsing must have failed. */
if (error == NULL)
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to detect input data format.");
return NULL;
}
/* trim release entries to the desired size */
if (entry_limit > 0 && (guint) entry_limit < releases->len)
g_ptr_array_remove_range (releases, entry_limit, releases->len - entry_limit);
/* mark only the desired amount of stuff as translatable */
if (translatable_limit >= 0) {
for (guint i = 0; i < releases->len; i++) {
AsRelease *release = AS_RELEASE (g_ptr_array_index (releases, i));
if (i >= (guint) translatable_limit)
as_release_set_description_translatable (release, FALSE);
}
}
return releases;
}
/**
* as_news_to_releases_from_filename:
*
* Convert NEWS file to a list of AsRelease elements.
*/
GPtrArray *
as_news_to_releases_from_filename (const gchar *fname,
AsNewsFormatKind kind,
gint entry_limit,
gint translatable_limit,
GError **error)
{
g_autofree gchar *data = NULL;
/* try to guess what kind of file we are dealing with, assume YAML if detection fails */
if (kind == AS_NEWS_FORMAT_KIND_UNKNOWN) {
if (g_str_has_suffix (fname, ".yml") || g_str_has_suffix (fname, ".yaml"))
kind = AS_NEWS_FORMAT_KIND_YAML;
else if (g_str_has_suffix (fname, ".md"))
kind = AS_NEWS_FORMAT_KIND_MARKDOWN;
else if (g_str_has_suffix (fname, "NEWS") || g_str_has_suffix (fname, "news") ||
g_str_has_suffix (fname, ".txt"))
kind = AS_NEWS_FORMAT_KIND_TEXT;
else
kind = AS_NEWS_FORMAT_KIND_YAML;
}
/* load data from file */
if (!g_file_get_contents (fname, &data, NULL, error))
return NULL;
return as_news_to_releases_from_data (data, kind, entry_limit, translatable_limit, error);
}
/**
* as_releases_to_news_data:
*
* Convert a list of releases to a text representation.
*/
gboolean
as_releases_to_news_data (GPtrArray *releases,
AsNewsFormatKind kind,
gchar **news_data,
GError **error)
{
if (kind == AS_NEWS_FORMAT_KIND_YAML)
return as_news_releases_to_yaml (releases, news_data);
if (kind == AS_NEWS_FORMAT_KIND_TEXT || kind == AS_NEWS_FORMAT_KIND_MARKDOWN)
return as_news_releases_to_text (releases, news_data, kind);
g_set_error (error,
AS_METADATA_ERROR,
AS_METADATA_ERROR_FAILED,
"Unable to detect input data format.");
return FALSE;
}
/**
* as_releases_to_news_file:
*
* Convert a list of releases to a text representation and save it to a file.
*/
gboolean
as_releases_to_news_file (GPtrArray *releases,
const gchar *fname,
AsNewsFormatKind kind,
GError **error)
{
g_autofree gchar *data = NULL;
/* try to guess what kind of file we are supposed to be writing */
if (kind == AS_NEWS_FORMAT_KIND_UNKNOWN) {
if (g_str_has_suffix (fname, ".yml") || g_str_has_suffix (fname, ".yaml"))
kind = AS_NEWS_FORMAT_KIND_YAML;
else if (g_str_has_suffix (fname, ".md"))
kind = AS_NEWS_FORMAT_KIND_MARKDOWN;
else if (g_str_has_suffix (fname, "NEWS") || g_str_has_suffix (fname, "news") ||
g_str_has_suffix (fname, ".txt"))
kind = AS_NEWS_FORMAT_KIND_TEXT;
else
kind = AS_NEWS_FORMAT_KIND_YAML;
}
if (!as_releases_to_news_data (releases, kind, &data, error))
return FALSE;
return g_file_set_contents (fname, data, -1, error);
}