PageSourceSearch

https://faqword.com/media/regularlabs/js/script.js?4ee45f

js faqword.com collected 2026-10-02 16:22:57 UTC 29,236 bytes, 893 lines download raw bytes

1/**
2 * @package         Regular Labs Library
3 * @version         26.9.10756
4 * 
5 * @author          Peter van Westen <[email protected]>
6 * @link            https://regularlabs.com
7 * @copyright       Copyright © 2026 Regular Labs All Rights Reserved
8 * @license         GNU General Public License version 2 or later
9 */
10
11(function() {
12    'use strict';
13
14    window.RegularLabs = window.RegularLabs || {};
15
16    window.RegularLabs.Scripts = window.RegularLabs.Scripts || {
17        version: '26.9.10756',
18
19        ajax_list             : [],
20        started_ajax_list     : false,
21        ajax_list_timer       : null,
22        editor_api            : null,
23        editor_api_loading    : null,
24        legacy_editors        : null,
25        editor_selections     : {},
26
27        escapeHtml: function(value) {
28            return String(value)
29                .replace(/&/g, '&amp;')
30                .replace(/</g, '&lt;')
31                .replace(/>/g, '&gt;');
32        },
33
34        escapeAttributeValue: function(value) {
35            return this.escapeHtml(value)
36                .replace(/"/g, '&quot;');
37        },
38
39        escapeTagAttributeValue: function(value, use_backslash_quotes = false) {
40            const quote = use_backslash_quotes ? '\\"' : '&quot;';
41
42            return this.escapeHtml(value)
43                .replace(/"/g, quote);
44        },
45
46        getOptions: function(key, default_value = {}) {
47            if (typeof Joomla !== 'undefined' && typeof Joomla.getOptions !== 'undefined') {
48                return Joomla.getOptions(key, default_value);
49            }
50
51            if (typeof Joomla === 'undefined' || typeof Joomla.optionsStorage === 'undefined') {
52                return default_value;
53            }
54
55            return typeof Joomla.optionsStorage[key] === 'undefined'
56                ? default_value
57                : Joomla.optionsStorage[key];
58        },
59
60        parseJson: function(value, default_value = null) {
61            if (typeof value !== 'string' || value === '') {
62                return default_value;
63            }
64
65            try {
66                return JSON.parse(value);
67            } catch (error) {
68                return default_value;
69            }
70        },
71
72        parseJsonObject: function(value, default_value = {}) {
73            const parsed = this.parseJson(value, default_value);
74
75            if ( ! parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
76                return default_value;
77            }
78
79            return parsed;
80        },
81
82        getEditor: function(editor) {
83            if (typeof editor !== 'string') {
84                return editor;
85            }
86
87            if (this.editor_api) {
88                const api_editor = this.editor_api.get(editor);
89
90                if (api_editor) {
91                    return api_editor;
92                }
93            }
94
95            if (this.legacy_editors) {
96                return this.legacy_editors[editor] || null;
97            }
98
99            return typeof Joomla !== 'undefined'
100                ? Joomla.editors?.instances?.[editor] || null
101                : null;
102        },
103
104        getEditors: function() {
105            if ( ! this.editor_api) {
106                return this.legacy_editors || {};
107            }
108
109            const editors = {};
110
111            document.querySelectorAll('textarea[id]').forEach((element) => {
112                const editor = this.editor_api.get(element.id);
113
114                if (editor) {
115                    editors[element.id] = editor;
116                }
117            });
118
119            const active_editor = this.editor_api.getActive();
120
121            if (active_editor) {
122                editors[this.getEditorId(active_editor)] = active_editor;
123            }
124
125            return editors;
126        },
127
128        initEditorApi: function() {
129            if (this.editor_api_loading || typeof Joomla === 'undefined') {
130                return;
131            }
132
133            this.editor_api_loading = import('editor-api')
134                .then(({JoomlaEditor}) => {
135                    this.editor_api = JoomlaEditor;
136                })
137                .catch(() => {
138                    this.legacy_editors = Joomla.editors && Joomla.editors.instances
139                        ? Joomla.editors.instances
140                        : {};
141                });
142        },
143
144        getEditorValue: function(editor) {
145            return this.getEditor(editor)?.getValue();
146        },
147
148        getEditorFieldValue: function(element) {
149            const editor_value = this.getEditorValue(element.name || element.i
149d);
150
151            if (typeof editor_value !== 'undefined') {
152                return editor_value;
153            }
154
155            const code_mirror = element.closest('joomla-editor-codemirror')?.querySelector('.CodeMirror');
156
157            return code_mirror?.CodeMirror?.getValue();
158        },
159
160        setEditorValue: function(editor, value) {
161            this.getEditor(editor)?.setValue(value);
162        },
163
164        isEditorSourceMode: function(editor) {
165            editor = this.getEditor(editor);
166
167            if ( ! editor) {
168                return false;
169            }
170
171            const source_instance = editor.instance?.plugins?.source;
172
173            return Boolean(
174                source_instance
175                && typeof source_instance.isHidden === 'function'
176                && ! source_instance.isHidden()
177            );
178        },
179
180        isJceEditor: function(editor) {
181            editor = this.getEditor(editor);
182
183            if ( ! editor) {
184                return false;
185            }
186
187            const source_instance = editor.instance?.plugins?.source;
188
189            return Boolean(source_instance && typeof source_instance.isHidden === 'function');
190        },
191
192        getEditorId: function(editor) {
193            editor = this.getEditor(editor);
194
195            if ( ! editor) {
196                return '';
197            }
198
199            if (typeof editor.id === 'function') {
200                return editor.id();
201            }
202
203            return editor.id || editor.instance?.id || editor.instance?.editor?.id || '';
204        },
205
206        getEditorSourceElement: function(editor) {
207            editor = this.getEditor(editor);
208
209            if ( ! this.isEditorSourceMode(editor)) {
210                return null;
211            }
212
213            return editor.instance.getElement().ownerDocument.getElementById(
214                editor.instance.id + '_editor_source_textarea'
215            );
216        },
217
218        getEditorTextarea: function(editor) {
219            editor = this.getEditor(editor);
220
221            if ( ! editor) {
222                return null;
223            }
224
225            const source_element = this.getEditorSourceElement(editor);
226
227            if (source_element) {
228                return source_element;
229            }
230
231            const instance_element = editor.instance?.editor;
232
233            if (instance_element && typeof instance_element.selectionStart !== 'undefined') {
234                return instance_element;
235            }
236
237            if (typeof editor.instance?.getElement !== 'function') {
238                return null;
239            }
240
241            const editor_element = editor.instance.getElement();
242
243            return editor_element?.classList?.contains('wf-no-editor')
244                ? editor_element
245                : null;
246        },
247
248        captureEditorSelection: function(editor) {
249            const editor_name = typeof editor === 'string' ? editor : this.getEditorId(editor);
250            editor = this.getEditor(editor);
251
252            if ( ! editor) {
253                return;
254            }
255
256            const editor_id = this.getEditorId(editor) || editor_name;
257
258            if ( ! editor_id) {
259                return;
260            }
261
262            if (this.editor_selections[editor_id]?.preserve) {
263                return;
264            }
265
266            const textarea = this.getEditorTextarea(editor);
267
268            if (textarea) {
269                this.editor_selections[editor_id] = {
270                    end  : textarea.selectionEnd,
271                    start: textarea.selectionStart,
272                    value: textarea.value.substring(
273                        textarea.selectionStart,
274                        textarea.selectionEnd
275                    ),
276                };
277                return;
278            }
279
280            if (typeof editor.instance?.selection !== 'undefined') {
281                this.editor_selections[editor_id] = {
282                    bookmark: editor.instance.selection.getBookmark(2, true),
283                    html    : editor.instance.selection.getContent(),
284                    range   : editor.instance.selection.getRng().cloneRange(),
285                    value   : editor.instance.selection.getContent({format: 'text'}),
286                };
287                return;
288            }
289
290            const fallback_textarea = document.getElementById(editor_name);
291
292            if (fallback_textarea && typeof fallback_textarea.selectionStart !== 'undefined') {
293                this.editor_selections[editor_id] = {
294                    end  : fallback_textarea.selectionEnd,
295                    start: fallback_textarea.selectionStart,
296                    value: fallback_textarea.value.substring(
297                        fallback_textarea.selectionStart,
298                        fallback_textarea.selectionEnd
299                    ),
300                };
301            }
302        },
303
304        selectEditorText: function(editor, text) {
305            const editor_name = typeof editor === 'string' ? editor : this.getEditorId(editor);
306            editor = this.getEditor(editor);
307
308            if ( ! editor) {
309                return '';
310            }
311
312            const body = editor.instance?.getBody?.();
313            const selection_element = this.getEditorTextarea(editor)
314                || (body ? null : document.getElementById(editor_name));
315
316            if (selection_element && typeof selection_element.selectionStart !== 'undefined') {
317                const start = selection_element.value.indexOf(text);
318
319                if (start === -1) {
320                    return '';
321                }
322
323                selection_element.focus();
324                selection_element.setSelectionRange(start, start + text.length);
325                this.editor_selections[this.getEditorId(editor) || editor_name] = {
326                    end     : start + text.length,
327                    preserve: true,
328                    start   : start,
329                    value   : text,
330                };
331
332                return selection_element.value.substring(
333                    selection_element.selectionStart,
334                    selection_element.selectionEnd
335                );
336            }
337
338            if ( ! body || typeof editor.instance?.selection === 'undefined') {
339                return '';
340            }
341
342            const editor_document = body.ownerDocument;
343            const walker = editor_document.createTreeWalker(
344                body,
345                editor_document.defaultView.NodeFilter.SHOW_TEXT
346            );
347            let text_node = null;
348
349            while (walker.nextNode()) {
350                if (walker.currentNode.data.includes(text)) {
351                    text_node = walker.currentNode;
352                    break;
353                }
354            }
355
356            if ( ! text_node) {
357                return '';
358            }
359
360            const start = text_node.data.indexOf(text);
361            const range = editor_document.createRange();
362            range.setStart(text_node, start);
363            range.setEnd(text_node, start + text.length);
364            editor.instance.focus();
365            editor.instance.selection.setRng(range);
366            const html_container = editor_document.createElement('div');
367            html_container.appendChild(range.cloneContents());
368            this.editor_selections[this.getEditorId(editor) || editor_name] = {
369                html    : html_container.innerHTML,
370                preserve: true,
371                range   : range.cloneRange(),
372                value   : text,
373            };
374
375            return text;
376        },
377
378        isEditorActive: function(editor) {
379            editor = this.getEditor(editor);
380
381            if ( ! editor) {
382                return false;
383            }
384
385            const textarea = this.getEditorTextarea(editor);
386
387            if (textarea) {
388                return textarea.ownerDocument.activeElement === textarea;
389            }
390
391            if (typeof editor.instance?.hasFocus === 'function') {
392                if (editor.instance.hasFocus()) {
393                    return true;
394                }
395            }
396
397            if (editor.instance?.focused) {
398                return true;
399            }
400
401            if (typeof editor.instance?.getDoc === 'function' && editor.instance.getDoc()?.hasFocus()) {
402                return true;
403            }
404
405            if (typeof editor.instance?.getContentAreaContainer === 'function') {
406                const iframe = editor.instance.getContentAreaContainer()?.querySelector('iframe');
407
408                if (iframe && iframe.ownerDocument.activeElement === iframe) {
409                    return true;
410                }
411            }
412
413            const fallback_textarea = editor.instance?.editor;
414
415            return Boolean(
416                fallback_textarea
417                && fallback_textarea.ownerDocument.activeElement === fallback_textarea
418            );
419        },
420
421        captureActiveEditorSelections: function() {
422            if (document.querySelector(
423                '.joomla-modal.show iframe[src*="Plugin.EditorButton."], '
424                + 'dialog[open] iframe[src*="Plugin.EditorButton."]'
425            )) {
426                return;
427            }
428
429            if (this.editor_api) {
430                const editor = this.editor_api.getActive();
431
432                if (editor) {
433                    this.captureEditorSelection(editor);
434                }
435
436                return;
437            }
438
439            Object.values(this.legacy_editors || {}).forEach((editor) => {
440                if (this.isEditorActive(editor)) {
441                    this.captureEditorSelection(editor);
442                }
443            });
444        },
445
446        captureEditorSelections: function(only_untracked = false) {
447            Object.values(this.getEditors()).forEach((editor) => {
448                if (only_untracked && this.editor_selections[this.getEditorId(editor)]) {
449                    return;
450                }
451
452                this.captureEditorSelection(editor);
453            });
454        },
455
456        initEditorSelectionTracking: function() {
457            if (document.documentElement.dataset.rlEditorSelectionTracking === 'true') {
458                return;
459            }
460
461            document.documentElement.dataset.rlEditorSelectionTracking = 'true';
462            this.initEditorApi();
463            document.addEventListener('pointerdown', (event) => {
464                this.captureEditorSelectionFromEvent(event);
465            }, true);
466            document.addEventListener('keydown', (event) => {
467                if (event.key !== 'Enter' && event.key !== ' ') {
468                    return;
469                }
470
471                this.captureEditorSelectionFromEvent(event);
472            }, true);
473        },
474
475        captureEditorSelectionFromEvent: function(event) {
476            if ( ! this.isEditorButtonElement(event.target)) {
477                return;
478            }
479
480            this.captureActiveEditorSelections();
481            this.captureEditorSelections(true);
482        },
483
484        isEditorButtonElement: function(target) {
485            const element = target instanceof Element ? target.closest('button, [role="menuitem"]') : null;
486
487            if ( ! element) {
488                return false;
489            }
490
491            const is_regular_labs_button = element.matches(
492                '[data-joomla-editor-button-options*="Plugin.EditorButton."]'
493            );
494            const is_marked_editor_button = element.matches('[data-rl-editor-button]');
495            const is_legacy_editor_button = Boolean(element.closest('.editor-xtd-buttons'));
496            const is_editor_button_menu = element.getAttribute('data-mce-name') === 'jxtdbuttons'
497                || element.getAttribute('aria-label') === 'CMS Content';
498
499            if (
500                ! is_regular_labs_button
501                && ! is_marked_editor_button
502                && ! is_legacy_editor_button
503                && ! is_editor_button_menu
504            ) {
505                return false;
506            }
507
508            return true;
509        },
510
511        getEditorSelection: function(editor, preserve_html = false) {
512            editor = this.getEditor(editor);
513
514            if ( ! editor) {
515                return '';
516            }
517
518            const textarea = this.getEditorTextarea(editor);
519
520            if (this.isEditorActive(editor)) {
521                if (textarea) {
522                    return textarea.value.substring(
523                        textarea.selectionStart,
524                        textarea.selectionEnd
525                    );
526                }
527
528                if (preserve_html && typeof editor.instance?.selection !== 'undefined') {
529                    return editor.instance.selection.getContent();
530                }
531
532                return editor.getSelection();
533            }
534
535            const selection = this.editor_selections[this.getEditorId(editor)];
536            if (selection) {
537                if (preserve_html && typeof selection.html !== 'undefined') {
538                    return selection.html;
539                }
540
541                if (typeof selection.value !== 'undefined') {
542                    return selection.value;
543                }
544            }
545
546            if (textarea) {
547                return textarea.value.substring(
548                    textarea.selectionStart,
549                    textarea.selectionEnd
550                );
551            }
552
553            if (preserve_html && typeof editor.instance?.selection !== 'undefined') {
554                return editor.instance.selection.getContent();
555            }
556
557            return editor.getSelection();
558        },
559
560        replaceEditorSelection: function(editor, value) {
561            editor = this.getEditor(editor);
562
563            if ( ! editor) {
564                return;
565            }
566
567            const editor_id = this.getEditorId(editor);
568            const selection = this.editor_selections[editor_id];
569            const selection_element = this.getEditorTextarea(editor);
570
571            if (
572                selection?.range
573                && ! selection_element
574                && typeof editor.instance?.selection !== 'undefined'
575            ) {
576                editor.instance.focus();
577                editor.instance.selection.setRng(selection.range);
578            } else if (
579                selection?.bookmark
580                && ! selection_element
581                && typeof editor.instance?.selection !== 'undefined'
582            ) {
583                editor.instance.focus();
584                editor.instance.selection.moveToBookmark(selection.bookmark);
585            } else if (typeof selection?.start !== 'undefined') {
586                if (selection_element) {
587                    selection_element.focus();
588                    selection_element.setSelectionRange(selection.start, selection.end);
589                }
590            }
591
592            if (
593                selection?.range
594                && ! selection_element
595                && this.isJceEditor(editor)
596                && ! this.isEditorSourceMode(editor)
597            ) {
598                editor.instance.execCommand('mceInsertContent', false, value);
599                delete this.editor_selections[editor_id];
600                return;
601            }
602
603            editor.replaceSelection(value);
604            delete this.editor_selections[editor_id];
605        },
606
607        isEditorReady: function(editor) {
608            editor = this.getEditor(editor);
609
610            if ( ! editor || typeof editor.getValue !== 'function') {
611                return false;
612            }
613
614            if (typeof editor.instance?.initialized === 'boolean' && ! editor.instance.initialized) {
615                return false;
616            }
617
618            try {
619                editor.getValue();
620                return true;
621            } catch (error) {
622                return false;
623            }
624        },
625
626        normalizeEditorInsertionValue: function(editor, value) {
627            editor = this.getEditor(editor);
628
629            if ( ! editor) {
630                return value;
631            }
632
633            if (
634                editor.className !== 'mce_editable'
635                || value.substring(0, 3) !== '<p>'
636                || value.substring(value.length - 4) !== '</p>'
637            ) {
638                return value;
639            }
640
641            return value.substring(3, value.length - 4);
642        },
643
644        applyEditorFormatting: function(editor_name) {
645            if (typeof tinyMCE === 'undefined') {
646                return;
647            }
648
649            tinyMCE.get(editor_name)?.formatter?.apply();
650        },
651
652        prepareOutputForEditor: function(string, editor, remove_all_p_tags = false) {
653            editor = this.getEditor(editor);
654
655            if ( ! editor) {
656                return string;
657            }
658
659            const editor_content   = editor.getValue();
660            const editor_selection = this.getEditorSelection(editor);
661
662            // If the editor is CodeMirror
663            if (editor_content === '' || editor_content[0] !== '<') {
664                return remove_all_p_tags ? string.replace(/<\/?p>/g, '') : string;
665            }
666
667            // If selection is empty or code is replacing a selection not starting with a html tag
668            if (editor_selection.indexOf('<') !== 0) {
669                // remove surrounding p tags
670                return string.replace(/^<p>(.*)<\/p>$/g, '$1');
671            }
672
673            return string;
674        },
675
676        getFormValue: function(form, id, default_value = '', getEditorContent = null) {
677            let elements = form.querySelectorAll('[name="' + id + '"]');
678
679            if ( ! elements.length) {
680                elements = form.querySelectorAll('[name="' + id + '[]"]');
681            }
682
683            if ( ! elements.length) {
684                return default_value;
685            }
686
687            const element = elements[0];
688
689            if (element.type === 'textarea') {
690                const value = getEditorContent ? getEditorContent(element) : element.value;
691
692                return this.fixValueType(value);
693            }
694
695            let value = element.value ? element.value : default_value;
696
697            if (element.type === 'select-one') {
698                if (element.type === 'checkbox' && ! element.checked) {
699                    return default_value;
700                }
701
702                return this.fixValueType(value);
703            }
704
705            if (element.type === 'select-multiple') {
706                value = [];
707
708                for (let i = 0; i < element.options.length; i++) {
709                    if (element.options[i].selected && element.options[i].value !== '') {
710                        value.push(element.options[i].value);
711                    }
712                }
713
714                return this.fixValueType(value);
715            }
716
717            if (elements.length > 1) {
718                value = [];
719
720                for (let i = 0; i < elements.length; i++) {
721                    if ((elements[i].selected || elements[i].checked) && elements[i].value !== '') {
722                        value.push(elements[i].value);
723                    }
724                }
725
726                if (element.type === 'radio') {
727                    return this.fixValueType(value[0]);
728                }
729
730                return this.fixValueType(value);
731            }
732
733            return this.fixValueType(value);
734        },
735
736        fixValueType: function(value) {
737            if (Array.isArray(value)) {
738                return value.map((val) => this.fixValueType(val));
739            }
740
741            if (isNaN(value) || isNaN(parseInt(value))) {
742                return value;
743            }
744
745            return Number(value);
746        },
747
748        loadAjax: function(url, success, fail, query, timeout, dataType, cache) {
749            if (url.indexOf('index.php') !== 0 && url.indexOf('administrator/index.php') !== 0) {
750                url = url.replace('http://', '');
751                url = `index.php?rl_qp=1&url=${encodeURIComponent(url)}`;
752                if (timeout) {
753                    url += `&timeout=${timeout}`;
754                }
755                if (cache) {
756                    url += `&cache=${cache}`;
757                }
758            }
759
760            let base = window.location.pathname;
761
762            base = base.substring(0, base.lastIndexOf('/'));
763
764            if (
765                typeof Joomla !== 'undefined'
766                && typeof Joomla.getOptions !== 'undefined'
767                && Joomla.getOptions('system.paths')
768            ) {
769                base = Joomla.getOptions('system.paths').base;
770            }
771
772            const token = typeof Joomla !== 'undefined' && typeof Joomla.getOptions === 'function'
773                ? Joomla.getOptions('csrf.token', '')
774                : '';
775            const headers = token ? {'X-CSRF-Token': token} : {};
776
777            // console.log(url);
778            // console.log(`${base}/${url}`);
779
780            this.loadUrl(
781                `${base}/${url}`,
782                query,
783                (function(data) {
784                    if (success) {
785                        success = `data = data ? data : ''; ${success};`.replace(/;\s*;/g, ';');
786                        eval(success);
787                    }
788                }),
789                (function(data) {
790                    if (fail) {
791                        fail = `data = data ? data : ''; ${fail};`.replace(/;\s*;/g, ';');
792                        eval(fail);
793                    }
794                }),
795                headers
796            );
797        },
798
799        /**
800         * Loads a url with optional POST data and optionally calls a function on success or fail.
801         *
802         * @param url      String containing the url to load.
803         * @param data     Optional string representing the POST data to send along.
804         * @param success  Optional callback function to execute when the url loads successfully (status 200).
805         * @param fail     Optional callback function to execute when the url fails to load.
806         * @param headers  Optional request headers keyed by name.
807         */
808        loadUrl: function(url, data, success, fail, headers = {}) {
809            return new Promise((resolve) => {
810                const request = new XMLHttpRequest();
811
812                request.open("POST", url, true);
813
814                request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
815
816                for (const [headerName, headerValue] of Object.entries(headers)) {
817                    request.setRequestHeader(headerName, headerValue);
818                }
819
820                request.onreadystatechange = function() {
821                    if (this.readyState !== 4) {
822                        return;
823                    }
824
825                    if (this.status !== 200) {
826                        fail && fail.call(null, this.responseText, this.status, this);
827                        resolve(this);
828                        return;
829                    }
830
831                    success && success.call(null, this.responseText, this.status, this);
832                    resolve(this);
833                };
834
835                request.send(data);
836            });
837        },
838
839        addToLoadAjaxList: function(url, success, error, query = '') {
840            // wrap inside the loadajax function (and escape string values)
841            url     = url.replace(/'/g, "\\'");
842            success = success.replace(/'/g, "\\'");
843            error   = error.replace(/'/g, "\\'");
844            query   = query.replace(/'/g, "\\'");
845
846            const action = `RegularLabs.Scripts.loadAjax(
847                    '${url}',
848                    '${success};RegularLabs.Scripts.ajaxRun();',
849                    '${error};RegularLabs.Scripts.ajaxRun();',
850                    '${query}'
851                )`;
852
853            this.addToAjaxList(action);
854        },
855
856        addToAjaxList: function(action) {
857            this.ajax_list.push(action);
858
859            if ( ! this.started_ajax_list) {
860                this.ajaxRun();
861            }
862        },
863
864        ajaxRun: function() {
865            if ( ! this.ajax_list.length) {
866                return;
867            }
868
869            clearTimeout(this.ajax_list_timer);
870
871            this.started_ajax_list = true;
872
873            const action = this.ajax_list.shift();
874
875            eval(`${action};`);
876
877            if ( ! this.ajax_list.length) {
878                this.started_ajax_list = false;
879                return;
880            }
881
882            // Re-trigger this ajaxRun function just in case it hangs somewhere
883            this.ajax_list_timer = setTimeout(
884                function() {
885                    RegularLabs.Scripts.ajaxRun();
886                },
887                5000
888            );
889        },
890    };
891
892    RegularLabs.Scripts.initEditorSelectionTracking();
893})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.