Обработка графемных кластеров в Dart - PullRequest
0 голосов
/ 01 февраля 2019

Из того, что я могу сказать, у Dart нет поддержки кластеров графем, хотя есть разговоры о его поддержке:

До тех пор, пока он не будет реализован, каковы мои варианты перебора кластеров графем?Например, если у меня есть строка, подобная этой:

String family = '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}'; // ?‍?‍?
String myString = 'Let me introduce my $family to you.';

, и после семейства emoji с пятью кодами есть курсор:

enter image description here

Как бы я переместил курсор на один воспринимаемый пользователем символ влево?

(В данном конкретном случае я знаю размер кластера графем, чтобы я мог это сделать, но я действительно спрашиваю о том, как найти длину произвольно длинного кластера графем.)

Обновление

Из этой статьи я вижу, что Swift использует системную библиотеку ICU .Нечто подобное может быть возможно во Flutter.

Дополнительный код

Для тех, кто хочет поиграть с моим примером выше, вот демонстрационный проект.Кнопки перемещают курсор вправо или влево.В настоящее время требуется 8 нажатий кнопок для перемещения курсора мимо семейства смайликов.

enter image description here

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Grapheme cluster testing')),
        body: BodyWidget(),
      ),
    );
  }
}

class BodyWidget extends StatefulWidget {
  @override
  _BodyWidgetState createState() => _BodyWidgetState();
}

class _BodyWidgetState extends State<BodyWidget> {

  TextEditingController controller = TextEditingController(
      text: 'Let me introduce my \u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467} to you.'
  );

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          controller: controller,
        ),
        Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                child: Text('<<'),
                onPressed: () {
                  _moveCursorLeft();
                },
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                child: Text('>>'),
                onPressed: () {
                  _moveCursorRight();
                },
              ),
            ),
          ],
        )
      ],
    );
  }

  void _moveCursorLeft() {
    int currentCursorPosition = controller.selection.start;
    if (currentCursorPosition == 0)
      return;
    int newPosition = currentCursorPosition - 1;
    controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
  }

  void _moveCursorRight() {
    int currentCursorPosition = controller.selection.end;
    if (currentCursorPosition == controller.text.length)
      return;
    int newPosition = currentCursorPosition + 1;
    controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
  }
}

Ответы [ 2 ]

0 голосов
/ 06 июля 2019

Этот исходный код из класса TextPainter дает некоторые подсказки, как найти кластеры графем.В частности, длинные кластеры графем создаются с помощью соединителя нулевой ширины для соединения точек кода, поэтому вы можете использовать эти знания для поиска конца кластера графем.

  // Unicode value for a zero width joiner character.
  static const int _zwjUtf16 = 0x200d;

  // Get the Rect of the cursor (in logical pixels) based off the near edge
  // of the character upstream from the given string offset.
  // TODO(garyq): Use actual extended grapheme cluster length instead of
  // an increasing cluster length amount to achieve deterministic performance.
  Rect _getRectFromUpstream(int offset, Rect caretPrototype) {
    final String flattenedText = _text.toPlainText();
    final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
    if (prevCodeUnit == null)
      return null;

    // Check for multi-code-unit glyphs such as emojis or zero width joiner
    final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16;
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
    while (boxes.isEmpty && flattenedText != null) {
      final int prevRuneOffset = offset - graphemeClusterLength;
      boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
      // When the range does not include a full cluster, no boxes will be returned.
      if (boxes.isEmpty) {
        // When we are at the beginning of the line, a non-surrogate position will
        // return empty boxes. We break and try from downstream instead.
        if (!needsSearch)
          break; // Only perform one iteration if no search is required.
        if (prevRuneOffset < -flattenedText.length)
          break; // Stop iterating when beyond the max length of the text.
        // Multiply by two to log(n) time cover the entire text span. This allows
        // faster discovery of very long clusters and reduces the possibility
        // of certain large clusters taking much longer than others, which can
        // cause jank.
        graphemeClusterLength *= 2;
        continue;
      }
      final TextBox box = boxes.first;

      // If the upstream character is a newline, cursor is at start of next line
      const int NEWLINE_CODE_UNIT = 10;
      if (prevCodeUnit == NEWLINE_CODE_UNIT) {
        return Rect.fromLTRB(_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
      }

      final double caretEnd = box.end;
      final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
      return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
    }
    return null;
  }

Также здесьфайл в библиотеке minikin движка libtxt Flutter, который работает с кластерами Grapheme.Я не уверен, что он доступен напрямую, но может быть полезным для справки.

0 голосов
/ 03 марта 2019

Обновление: используйте https://pub.dartlang.org/packages/icu

Пример кода:

import 'package:flutter/material.dart';


import 'dart:async';
import 'package:icu/icu.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Grapheme cluster testing')),
        body: BodyWidget(),
      ),
    );
  }
}

class BodyWidget extends StatefulWidget {
  @override
  _BodyWidgetState createState() => _BodyWidgetState();
}

class _BodyWidgetState extends State<BodyWidget> {
  final ICUString icuText = ICUString('Let me introduce my \u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467} to you.\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}');
  TextEditingController controller;
  _BodyWidgetState() {
    controller = TextEditingController(
      text: icuText.toString()
  );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextField(
          controller: controller,
        ),
        Row(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                child: Text('<<'),
                onPressed: () async {
                  await _moveCursorLeft();
                },
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                child: Text('>>'),
                onPressed: () async {
                  await _moveCursorRight();
                },
              ),
            ),
          ],
        )
      ],
    );
  }

  void _moveCursorLeft() async {
    int currentCursorPosition = controller.selection.start;
    if (currentCursorPosition == 0)
      return;
    int newPosition = await icuText.previousGraphemePosition(currentCursorPosition);
    controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
  }

  void _moveCursorRight() async {
    int currentCursorPosition = controller.selection.end;
    if (currentCursorPosition == controller.text.length)
      return;
    int newPosition = await icuText.nextGraphemePosition(currentCursorPosition);
    controller.selection = TextSelection(baseOffset: newPosition, extentOffset: newPosition);
  }
}


Оригинальный ответ:

Пока Dart / Flutter полностью не реализует ICU, я думаю, что вашЛучше всего использовать PlatformChannel для передачи нативной строки Unicode (iOS Swift4 + или Android Java / Kotlin) для итерации / манипуляции там и отправки результата.

  • Для Swift4 +Это готовая к использованию статья, о которой вы упомянули (не Swift3-, не ObjC)
  • Для Java / Kotlin замените Oracle BreakIterator на ICU library ,который работает намного лучше.Никаких изменений, кроме операторов импорта.

Причина, по которой я предлагаю использовать нативную манипуляцию (вместо того, чтобы делать это на Dart), заключается в том, что в Юникоде слишком много вещей для обработки, таких как нормализация, каноническая эквивалентность, ZWNJ, ZWJ, ZWSP и т. Д.

Прокомментируйте, если вам нужен пример кода.

...