1 /*
2  * xmlsnippets.c - this file is part of XMLSnippets, a Geany plugin
3  *
4  * Copyright 2010 Eugene Arshinov <earshinov(at)gmail(dot)com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 # include "config.h"
24 #endif
25 
26 #include "xmlsnippets.h"
27 #include <ctype.h>
28 #include <string.h>
29 
30 
31 typedef struct Info
32 {
33 	struct
34 	{
35 		const gchar *completion;
36 		const gchar *tag_name_start; /* start of the first tag within `completion' */
37 	} snippet;
38 	struct
39 	{
40 		const gchar *tag_name_end;
41 	} input;
42 } Info;
43 
44 
skip_xml_tag_name(const gchar * start)45 static const gchar * skip_xml_tag_name(const gchar * start)
46 {
47 	const gchar *iter;
48 
49 	for (iter = start; strchr(":_-.", *iter) || isalnum(*iter); iter++) ;
50 	return iter;
51 }
52 
53 
54 /* If user entered some attributes, copy them to the first tag within snippet body
55  * @return   Newly allocated completion string or @c NULL to indicate
56  *           that completion should be aborted
57  */
merge_attributes(const gchar * sel,gint size,const Info * info)58 static gchar * merge_attributes(const gchar *sel, gint size, const Info * info)
59 {
60 	const gchar *input_iter, *input_iter_end, *iter;
61 
62 	/* Separately ensure there is at least one space
63 	 * Needed for the code below which copy attributes */
64 	input_iter = info->input.tag_name_end;
65 	if (!isspace(*input_iter))
66 		goto normal; /* nothing to do */
67 
68 	input_iter++;
69 	while (isspace(*input_iter))
70 		input_iter++;
71 	if (*input_iter == '>')
72 		goto normal; /* nothing to do */
73 
74 	/* Find the end of the attribute string */
75 	g_assert(sel[size-1] == '>');
76 	input_iter_end = &sel[size-2];
77 	while (isspace(*input_iter_end))
78 		input_iter_end--;
79 	input_iter_end++;
80 
81 	/* Ensure the tag within snippet body does not contain attributes */
82 	iter = info->snippet.tag_name_start;
83 	iter = skip_xml_tag_name(iter);
84 	if (*iter != '>')
85 	{
86 		g_message("%s",
87 			"Autocompletion aborted: both of the input string and "
88 			"the first tag of the snippet body contain attributes");
89 		goto abort;
90 	}
91 
92 	/* Merge */
93 	{
94 		GString *completion = g_string_sized_new(20);
95 		g_string_append_len(completion, info->snippet.completion, iter - info->snippet.completion);
96 
97 		input_iter -= 1; /* leave one space */
98 		for (; input_iter != input_iter_end; ++input_iter)
99 		{
100 			switch (*input_iter)
101 			{
102 				case '{':
103 					g_string_append(completion, "{ob}");
104 					break;
105 				case '}':
106 					g_string_append(completion, "{cb}");
107 					break;
108 				case '%':
109 					g_string_append(completion, "{pc}");
110 					break;
111 				default:
112 					g_string_append_c(completion, *input_iter);
113 			}
114 		}
115 
116 		g_string_append(completion, iter);
117 		return g_string_free(completion, FALSE);
118 	}
119 
120 	abort:
121 		return NULL;
122 	normal:
123 		return g_strdup(info->snippet.completion);
124 }
125 
126 
get_completion(GeanyEditor * editor,const gchar * sel,const gint size,CompletionInfo * c,InputInfo * i)127 gboolean get_completion(GeanyEditor *editor, const gchar *sel, const gint size,
128 	CompletionInfo * c, InputInfo * i)
129 {
130 	Info info;
131 	const gchar *str_found, *tagname, *input_iter, *completion, *iter;
132 	gchar *completion_result;
133 
134 	g_return_val_if_fail(sel[size-1] == '>', FALSE);
135 	if (size < 3)
136 		return FALSE;
137 	if (sel[size - 2] == '/')
138 		return FALSE; /* closing tag */
139 
140 	str_found = utils_find_open_xml_tag_pos(sel, size);
141 	if (str_found == NULL)
142 		return FALSE;
143 
144 	tagname = str_found + 1; /* skip the left bracket */
145 	input_iter = skip_xml_tag_name(tagname);
146 	if (input_iter == tagname)
147 		return FALSE;
148 
149 	tagname = g_strndup(tagname, input_iter - tagname);
150 	completion = editor_find_snippet(editor, tagname);
151 	g_free((gchar *)tagname);
152 	if (completion == NULL)
153 		return FALSE;
154 
155 	/* To prevent insertion of a snippet, which user intended to use,
156 	 * e.g., in JavaScript, ensure the snippet body starts with a tag.
157 	 * We don't prevent name clashes by using special snippet names
158 	 * as it won't allow the plugin to automatically catch up the
159 	 * snippets supplied with Geany, e.g. "table". */
160 	iter = completion;
161 	while (TRUE)
162 	{
163 		if (isspace(*iter))
164 			iter++;
165 		else if (*iter == '\\' && (*(iter+1) == 'n' || *(iter+1) == 't'))
166 			iter += 2;
167 		else
168 			break;
169 	}
170 	if (*iter != '<')
171 		return FALSE;
172 
173 	info.snippet.completion = completion;
174 	info.snippet.tag_name_start = iter+1; /* +1: skip the left bracket */
175 	info.input.tag_name_end = input_iter;
176 
177 	completion_result = merge_attributes(sel, size, &info);
178 	if (completion_result == NULL)
179 		return FALSE;
180 
181 	c->completion = completion_result;
182 	i->tag_start = str_found - sel;
183 	return TRUE;
184 }
185