diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 00000000..0d54681f --- /dev/null +++ b/dartdoc_options.yaml @@ -0,0 +1,5 @@ +dartdoc: + warnings: + - unresolved-doc-reference + ignore: + - ambiguous-reexport diff --git a/lib/10.0/tizen.dart b/lib/10.0/tizen.dart index 408f9ff6..a4bfd76c 100644 --- a/lib/10.0/tizen.dart +++ b/lib/10.0/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 10.0. +library tizen_interop_10_0; import 'dart:ffi'; diff --git a/lib/6.0/tizen.dart b/lib/6.0/tizen.dart index 83806db2..5c6b0dbc 100644 --- a/lib/6.0/tizen.dart +++ b/lib/6.0/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 6.0. +library tizen_interop_6_0; import 'dart:ffi'; @@ -126,8 +127,7 @@ export '../../src/bindings/6.0/generated_bindings_appcore_agent.dart'; export '../../src/bindings/6.0/generated_bindings_asp.dart'; export '../../src/bindings/6.0/generated_bindings_badge.dart'; export '../../src/bindings/6.0/generated_bindings_bundle.dart'; -export '../../src/bindings/6.0/generated_bindings_calendar_service2.dart' - hide UnnamedUnion1, UnnamedStruct1; +export '../../src/bindings/6.0/generated_bindings_calendar_service2.dart'; export '../../src/bindings/6.0/generated_bindings_capi_appfw_alarm.dart'; export '../../src/bindings/6.0/generated_bindings_capi_appfw_app_common.dart'; export '../../src/bindings/6.0/generated_bindings_capi_appfw_app_control.dart'; @@ -229,8 +229,6 @@ export '../../src/bindings/6.0/generated_bindings_storage.dart'; export '../../src/bindings/6.0/generated_bindings_stt.dart'; export '../../src/bindings/6.0/generated_bindings_stt_engine.dart'; export '../../src/bindings/6.0/generated_bindings_tbm.dart'; -export '../../src/bindings/6.0/generated_bindings_time.dart' - hide UnnamedUnion1, UnnamedStruct1; export '../../src/bindings/6.0/generated_bindings_ttrace.dart'; export '../../src/bindings/6.0/generated_bindings_tts.dart'; export '../../src/bindings/6.0/generated_bindings_tts_engine.dart'; @@ -240,6 +238,8 @@ export '../../src/bindings/6.0/generated_bindings_vc_engine.dart'; export '../../src/bindings/6.0/generated_bindings_vc_manager.dart'; export '../../src/bindings/6.0/generated_bindings_wifi_direct.dart'; export '../../src/bindings/6.0/generated_bindings_yaca.dart'; +export '../../src/bindings/6.0/generated_bindings_time.dart' + hide UnnamedUnion1, UnnamedStruct1; final _lookupProvider = LookupProvider(); diff --git a/lib/6.5/tizen.dart b/lib/6.5/tizen.dart index 50db0292..b2dc9a37 100644 --- a/lib/6.5/tizen.dart +++ b/lib/6.5/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 6.5. +library tizen_interop_6_5; import 'dart:ffi'; @@ -238,8 +239,6 @@ export '../../src/bindings/6.5/generated_bindings_storage.dart'; export '../../src/bindings/6.5/generated_bindings_stt.dart'; export '../../src/bindings/6.5/generated_bindings_stt_engine.dart'; export '../../src/bindings/6.5/generated_bindings_tbm.dart'; -export '../../src/bindings/6.5/generated_bindings_time.dart' - hide UnnamedUnion1, UnnamedStruct1; export '../../src/bindings/6.5/generated_bindings_ttrace.dart'; export '../../src/bindings/6.5/generated_bindings_tts.dart'; export '../../src/bindings/6.5/generated_bindings_tts_engine.dart'; @@ -249,6 +248,8 @@ export '../../src/bindings/6.5/generated_bindings_vc_engine.dart'; export '../../src/bindings/6.5/generated_bindings_vc_manager.dart'; export '../../src/bindings/6.5/generated_bindings_wifi_direct.dart'; export '../../src/bindings/6.5/generated_bindings_yaca.dart'; +export '../../src/bindings/6.5/generated_bindings_time.dart' + hide UnnamedUnion1, UnnamedStruct1; final _lookupProvider = LookupProvider(); diff --git a/lib/7.0/tizen.dart b/lib/7.0/tizen.dart index cb96f86c..bb968cde 100644 --- a/lib/7.0/tizen.dart +++ b/lib/7.0/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 7.0. +library tizen_interop_7_0; import 'dart:ffi'; diff --git a/lib/8.0/tizen.dart b/lib/8.0/tizen.dart index d7abace1..c71cf830 100644 --- a/lib/8.0/tizen.dart +++ b/lib/8.0/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 8.0. +library tizen_interop_8_0; import 'dart:ffi'; diff --git a/lib/9.0/tizen.dart b/lib/9.0/tizen.dart index 086e8255..e8f8d693 100644 --- a/lib/9.0/tizen.dart +++ b/lib/9.0/tizen.dart @@ -1,4 +1,5 @@ -library tizen_interop; +/// Tizen Interop for Tizen 9.0. +library tizen_interop_9_0; import 'dart:ffi'; diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 450efd90..8d81528f 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -6,6 +6,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +/// @nodoc extension Int8Pointer on Pointer { int get length => cast().length; @@ -14,12 +15,14 @@ extension Int8Pointer on Pointer { } } +/// @nodoc extension StringInt8Pointer on String { Pointer toNativeInt8({Allocator allocator = malloc}) { return toNativeUtf8(allocator: allocator).cast(); } } +/// @nodoc extension CharPointer on Pointer { int get length => cast().length; @@ -28,6 +31,7 @@ extension CharPointer on Pointer { } } +/// @nodoc extension StringCharPointer on String { Pointer toNativeChar({Allocator allocator = malloc}) { return toNativeUtf8(allocator: allocator).cast(); diff --git a/pubspec.yaml b/pubspec.yaml index 41200aeb..1fc1f41c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,5 +12,6 @@ dependencies: dev_dependencies: ffigen: ^11.0.0 lints: ^3.0.0 + path: ^1.9.1 symgen: path: packages/symgen diff --git a/scripts/README.md b/scripts/README.md index 684781ed..eb66011f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -47,6 +47,24 @@ (see `CallbackDataCollector.type_substitute()` and maps used there: `KNOWN_TYPES`, `SPECIAL_TYPES`). * Run `./generate_callbacks.sh` to update `callbacks.cc` with callbacks data. +8. Convert Doxygen-style comments into Dartdoc format for the generated bindings: + + ```sh + dart run ./scripts/convert_description.dart + ``` + + This script will automatically process all `generated_bindings_*.dart` files inside the `lib/src/bindings/` directory. + +## Generating documentation + +The `generate_doc_script.py` script generates markdown API documentation for all supported Tizen versions. It scans the `configs` directory and creates or overwrites `doc/tizen_api.md` for each version. + +Run the script with: + +```sh +python3 scripts/generate_doc_script.py +``` + ## Handling Type Duplication Issues When splitting single binding code into library-specific binding codes from version 0.5.2 onwards, type duplication issues may occur between binding codes. Here are common issues and their solutions: @@ -154,13 +172,3 @@ type-map: export '../../src/bindings/6.0/generated_bindings_capi_media_camera.dart' hide UnnamedUnion1, UnnamedStruct1; ``` - -## Generating documentation - -The `generate_doc_script.py` script generates markdown API documentation for all supported Tizen versions. It scans the `configs` directory and creates or overwrites `doc/tizen_api.md` for each version. - -Run the script with: - -```sh -python3 scripts/generate_doc_script.py -``` diff --git a/scripts/convert_description.dart b/scripts/convert_description.dart new file mode 100644 index 00000000..25e3d131 --- /dev/null +++ b/scripts/convert_description.dart @@ -0,0 +1,1216 @@ +import 'dart:collection'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +const _doxygenTags = [ + 'brief', + 'details', + 'deprecated', + 'since(?:_tizen)?', + 'privlevel', + 'privilege', + 'remarks?', + 'param(?:\\[[^\\]]+\\])?', + 'return', + 'retval', + 'exception', + 'pre', + 'post', + 'note', + 'warning', + 'see', + 'par', + 'code', + 'endcode', + 'feature', + 'platform', + 'partner', + 'internal', + 'WEARABLE_ONLY', + 'section', + 'ingroup', + 'addtogroup', + 'typedef', + 'struct', + 'enum', +]; + +final _doxygenTagRegExp = RegExp( + r'^[\\@](' + '${_doxygenTags.join('|')}' + r')\b', +); +final _inlineDoxygenTagRegExp = RegExp( + r'[\\@](?:if|elseif|else|endif|ref|[abce])\b', +); +final _docCommentStartRegExp = RegExp(r'^(\s*)///'); +final _docCommentLineRegExp = RegExp(r'^\s*///'); +final _codeBlockStartRegExp = RegExp( + r'^[@\\]code(?:\{\.?([^}]+)\})?(?:\s+.*)?$', +); +final _versionedTizenLibraryPathRegExp = RegExp( + r'(^|[\\/])lib[\\/](\d+\.\d+)[\\/]tizen\.dart$', +); +final _listRegExp = RegExp(r'^\d+\.\s'); +final _topLevelDeclarationRegExp = RegExp( + r'^(typedef|(?:abstract|final|base)?\s*class|class|enum|extension|const|final|var|late)\s+', +); +final _primaryNativeClassRegExp = RegExp(r'^(?:final\s+)?class\s+Tizen[0-9]+Native\b'); +final _moduleBindingPathRegExp = RegExp( + r'(^|[\\/])lib[\\/]src[\\/]bindings[\\/](\d+\.\d+)[\\/]generated_bindings_(\w+)\.dart$', +); +final _mainClassRegExp = RegExp(r'^class\s+(Tizen\d+\w+)\b'); +final _alwaysHideFilesRegExp = RegExp( + r'(^|[\\/])lib[\\/]src[\\/](extensions|bindings[\\/]\d+\.\d+[\\/]generated_symbols)\.dart$', +); +final _publicTopLevelGetterRegExp = RegExp( + r'^[A-Za-z][A-Za-z0-9_<>,?. ]+\s+get\s+[A-Za-z][A-Za-z0-9_]*\b', +); +final _libraryTizenInteropRegExp = RegExp(r'^library\s+tizen_interop\s*;$'); +final _briefTagRegExp = RegExp(r'^[\\@]brief\s*(.*)$'); +final _detailsTagRegExp = RegExp(r'^[\\@]details\s*(.*)$'); +final _deprecatedTagRegExp = RegExp(r'^[\\@]deprecated\s*(.*)$'); +final _sinceTizenTagRegExp = RegExp(r'^[\\@]since_tizen\s*(.*)$'); +final _sinceTagRegExp = RegExp(r'^[\\@]since\s*(.*)$'); +final _privlevelTagRegExp = RegExp(r'^[\\@]privlevel\s*(.*)$'); +final _privilegeTagRegExp = RegExp(r'^[\\@]privilege\s*(.*)$'); +final _remarksTagRegExp = RegExp(r'^[\\@]remarks?\s*(.*)$'); +final _paramTagRegExp = RegExp(r'^[\\@]param(?:\[(.*?)\])?\s+(\S+)\s*(.*)$'); +final _returnTagRegExp = RegExp(r'^[\\@]return\s*(.*)$'); +final _retvalTagRegExp = RegExp(r'^[\\@]retval\s*(.*)$'); +final _exceptionTagRegExp = RegExp(r'^[\\@]exception\s*(.*)$'); +final _preTagRegExp = RegExp(r'^[\\@]pre\s*(.*)$'); +final _postTagRegExp = RegExp(r'^[\\@]post\s*(.*)$'); +final _noteTagRegExp = RegExp(r'^[\\@]note\s*(.*)$'); +final _warningTagRegExp = RegExp(r'^[\\@]warning\s*(.*)$'); +final _seeTagRegExp = RegExp(r'^[\\@]see\s*(.*)$'); +final _parTagRegExp = RegExp(r'^[\\@]par\s*(.*)$'); +final _featureTagRegExp = RegExp(r'^[\\@]feature\s*(.*)$'); +final _platformTagRegExp = RegExp(r'^[\\@]platform\s*(.*)$'); +final _partnerTagRegExp = RegExp(r'^[\\@]partner\s*(.*)$'); +final _internalTagRegExp = RegExp(r'^[\\@]internal\s*(.*)$'); +final _wearableOnlyTagRegExp = RegExp(r'^[\\@]WEARABLE_ONLY\s*(.*)$'); +final _sectionTagRegExp = RegExp(r'^[\\@]section\s*(.*)$'); +final _groupTagRegExp = RegExp(r'^[\\@](?:ingroup|addtogroup)\s*(.*)$'); +final _miscTagRegExp = RegExp(r'^[\\@](typedef|struct|enum)\s*(.*)$'); +final _namedDocItemRegExp = RegExp(r'^(\S+)\s*(.*)$'); +final _titleTrailingColonRegExp = RegExp(r':+$'); +final _encodedNewlineMarkerRegExp = RegExp(r'(? lines; + final String? title; + final String? language; +} + +class _StructuredDoc { + final List deprecations = []; + final List summary = []; + final List details = []; + final List since = []; + final List privilegeLevels = []; + final List privileges = []; + final List remarks = []; + final List<_DocItem> parameters = []; + final List returns = []; + final List<_DocItem> returnValues = []; + final List<_DocItem> exceptions = []; + final List preconditions = []; + final List postconditions = []; + final List notes = []; + final List warnings = []; + final List seeAlso = []; + final List<_CodeBlock> codeBlocks = []; + final LinkedHashMap> extraSections = LinkedHashMap(); + + void addExtra(String label, String text) { + extraSections.putIfAbsent(label, () => []).add(text); + } +} + +/// Converts Doxygen-style `///` comments in one or more Dart files in place. +void main(List args) { + if (args.isEmpty || args.contains('--help') || args.contains('-h')) { + stdout.writeln( + 'Usage: dart run scripts/convert_description.dart ' + '[more...]', + ); + exit(args.isEmpty ? 64 : 0); + } + + final pathsToProcess = []; + for (final arg in args) { + if (arg.startsWith('-')) { + stderr.writeln('Unknown option: $arg'); + exitCode = 64; + return; + } + + if (arg.endsWith('.dart')) { + pathsToProcess.add(arg); + } else { + // Treat argument as a version to fetch files from its bindings directory + final bindingDir = Directory('lib/src/bindings/$arg'); + if (bindingDir.existsSync()) { + for (final entry in bindingDir.listSync()) { + if (entry is File) { + final fileName = p.basename(entry.path); + if (fileName.endsWith('.dart')) { + pathsToProcess.add(entry.path); + } + } + } + } + final extensionsFile = File('lib/src/extensions.dart'); + if (extensionsFile.existsSync()) { + pathsToProcess.add(extensionsFile.path); + } + if (pathsToProcess.isEmpty) { + stderr.writeln( + 'Expected a Dart file path or version (e.g. "6.0"), got: $arg', + ); + exitCode = 64; + return; + } + } + } + + var changedCount = 0; + for (final path in pathsToProcess) { + try { + final changed = convertDoxygenCommentsInDartFile(path); + if (changed) { + stdout.writeln('Converted: $path'); + changedCount++; + } else { + stdout.writeln('No changes: $path'); + } + } on Object catch (error) { + stderr.writeln(error); + exitCode = 1; + return; + } + } + + if (changedCount == 0) { + stdout.writeln('No doxygen-style doc comments were rewritten.'); + } +} + +/// Rewrites Doxygen-style `///` lines into dartdoc/Markdown comments. +List convertDoxygenDocCommentLines(List docLines) { + return _convertMethodDocLines(docLines); +} + +/// Returns true when a doc-comment block contains recognized Doxygen markers. +bool looksLikeDoxygenDocCommentBlock(List docLines) { + var hasDoxygenTags = false; + + for (final line in docLines) { + final stripped = _stripGeneratedDocLine(line).trim(); + + // If it already looks like converted Dartdoc, don't process it again. + if (stripped.startsWith('**Since Tizen:**') || + stripped.startsWith('**Parameters:**') || + stripped.startsWith('**Returns:**') || + stripped.startsWith('**Remarks:**') || + stripped.startsWith('**Return values:**') || + stripped.startsWith('**See also:**') || + stripped.startsWith('**Example:**') || + stripped.contains('@nodoc') || + stripped.contains('{@category')) { + return false; + } + + if (line.trim().startsWith('<') || + _doxygenTagRegExp.hasMatch(stripped) || + _inlineDoxygenTagRegExp.hasMatch(stripped) || + RegExp(r'\[(in|out|in,out)\]').hasMatch(stripped) || + stripped.contains('```[')) { + hasDoxygenTags = true; + } + } + + return hasDoxygenTags; +} + +/// Converts all recognized Doxygen-style `///` blocks in a Dart source string. +String convertDoxygenCommentsInDartSource(String source, {String? path}) { + final newline = source.contains('\r\n') ? '\r\n' : '\n'; + final hasTrailingNewline = source.endsWith('\n'); + final lines = source.split(RegExp(r'\r?\n')).toList(); + if (hasTrailingNewline && lines.isNotEmpty && lines.last.isEmpty) { + lines.removeLast(); + } + + final output = []; + var index = 0; + while (index < lines.length) { + final line = lines[index]; + final docMatch = _docCommentStartRegExp.firstMatch(line); + if (docMatch == null) { + output.add(line); + index++; + continue; + } + + final indent = docMatch.group(1)!; + final block = []; + while ( + index < lines.length && _docCommentLineRegExp.hasMatch(lines[index])) { + block.add(lines[index]); + index++; + } + + final relativeBlock = block + .map((commentLine) => commentLine.substring(indent.length)) + .toList(); + if (!looksLikeDoxygenDocCommentBlock(relativeBlock)) { + output.addAll(block); + continue; + } + + final converted = convertDoxygenDocCommentLines(relativeBlock); + output.addAll(converted.map((commentLine) => '$indent$commentLine')); + } + + final annotatedOutput = _isModuleBindingFile(path) + ? _annotateModuleBindingDeclarations(output, path) + : _isAlwaysHideFile(path) + ? _annotateAllAsNodoc(output) + : _shouldHideTopLevelGeneratedBindingsDeclarations(path) + ? _annotateGeneratedBindingsTopLevelDeclarations(output) + : _shouldHideVersionedTizenLibraryGetters(path) + ? _annotateVersionedTizenLibraryGetters(output) + : output; + final normalizedOutput = _rewriteVersionedTizenLibraryName( + annotatedOutput, + path, + ); + final convertedSource = normalizedOutput.join(newline); + if (hasTrailingNewline) { + return '$convertedSource$newline'; + } + return convertedSource; +} + +/// Rewrites recognized Doxygen-style `///` blocks in `path` and returns +/// whether the file changed. +bool convertDoxygenCommentsInDartFile(String path) { + final file = File(path); + if (!file.existsSync()) { + throw StateError('Missing file: $path'); + } + + final original = file.readAsStringSync(); + final converted = convertDoxygenCommentsInDartSource(original, path: path); + if (converted == original) { + return false; + } + + file.writeAsStringSync(converted); + return true; +} + +bool _shouldHideTopLevelGeneratedBindingsDeclarations(String? path) { + if (path == null) { + return false; + } + + final fileName = p.basename(path); + return fileName == 'generated_bindings.dart'; +} + +bool _shouldHideVersionedTizenLibraryGetters(String? path) { + if (path == null) { + return false; + } + + return _versionedTizenLibraryPathRegExp.hasMatch(path); +} + +bool _isModuleBindingFile(String? path) { + if (path == null) { + return false; + } + return _moduleBindingPathRegExp.hasMatch(path); +} + +List _annotateModuleBindingDeclarations( + List lines, + String? path, +) { + if (path == null) { + return lines; + } + + final match = _moduleBindingPathRegExp.firstMatch(path); + if (match == null) { + return lines; + } + + final version = match.group(2)!; + final moduleName = match.group(3)!; + final versionId = version.replaceAll('.', '_'); + + String? mainClassName; + for (final line in lines) { + final classMatch = _mainClassRegExp.firstMatch(line); + if (classMatch != null) { + mainClassName = classMatch.group(1); + break; + } + } + + if (mainClassName == null) { + return lines; + } + + final categoryTizen = '$version/tizen'; + final output = []; + + // Add library directive if missing + if (!lines.any((l) => l.trim().startsWith('library '))) { + output.add('/// {@category $categoryTizen}'); + output.add('library tizen_interop_$versionId.$moduleName;'); + output.add(''); + } + + for (final line in lines) { + final trimmed = line.trimLeft(); + final isTopLevelLine = trimmed == line; + + if (isTopLevelLine && + _topLevelDeclarationRegExp.hasMatch(trimmed) && + !_primaryNativeClassRegExp.hasMatch(trimmed)) { + final isMainClass = _mainClassRegExp.hasMatch(trimmed); + + if (isMainClass) { + if (output.isEmpty || + !output.last.trim().contains('{@category $categoryTizen}')) { + output.add('/// {@category $categoryTizen}'); + } + } else { + if (output.isEmpty || !output.last.trim().contains('@nodoc')) { + output.add('/// @nodoc'); + } + } + } + + output.add(line); + } + + return output; +} + +bool _isAlwaysHideFile(String? path) { + if (path == null) { + return false; + } + return _alwaysHideFilesRegExp.hasMatch(path); +} + +List _annotateAllAsNodoc(List lines) { + final output = []; + for (final line in lines) { + final trimmed = line.trimLeft(); + final isTopLevelLine = trimmed == line; + + if (isTopLevelLine && _topLevelDeclarationRegExp.hasMatch(trimmed)) { + if (output.isEmpty || !output.last.trim().contains('@nodoc')) { + output.add('/// @nodoc'); + } + } + output.add(line); + } + return output; +} + +List _annotateGeneratedBindingsTopLevelDeclarations( + List lines, +) { + final output = []; + + for (final line in lines) { + final trimmed = line.trimLeft(); + final isTopLevelLine = trimmed == line; + + if (isTopLevelLine && + _topLevelDeclarationRegExp.hasMatch(trimmed) && + !_primaryNativeClassRegExp.hasMatch(trimmed)) { + if (output.isEmpty || !output.last.trim().contains('@nodoc')) { + output.add('/// @nodoc'); + } + } + + output.add(line); + } + + return output; +} + +List _annotateVersionedTizenLibraryGetters(List lines) { + final output = []; + + for (final line in lines) { + final trimmed = line.trimLeft(); + final isTopLevelLine = trimmed == line; + + if (isTopLevelLine && _publicTopLevelGetterRegExp.hasMatch(trimmed)) { + if (output.isEmpty || output.last.trim() != '/// {@nodoc}') { + output.add('/// {@nodoc}'); + } + } + + output.add(line); + } + + return output; +} + +List _rewriteVersionedTizenLibraryName( + List lines, + String? path, +) { + if (path == null) { + return lines; + } + + final match = _versionedTizenLibraryPathRegExp.firstMatch(path); + if (match == null) { + return lines; + } + + final versionId = match.group(2)!.replaceAll('.', '_'); + return lines + .map( + (line) => _libraryTizenInteropRegExp.hasMatch(line) + ? 'library tizen_interop_$versionId;' + : line, + ) + .toList(); +} + +List _convertMethodDocLines(List docLines) { + if (docLines.isEmpty) { + return const []; + } + + final doc = _parseStructuredDoc(docLines); + final output = []; + + void addParagraphs(List paragraphs) { + for (final paragraph in paragraphs) { + _appendDocParagraph(output, paragraph); + } + } + + void addBullets(String heading, List items) { + _appendDocBullets(output, heading, items); + } + + void addNamedBullets(String heading, List<_DocItem> items) { + _appendDocNamedBullets(output, heading, items); + } + + addParagraphs(doc.deprecations); + addParagraphs(doc.summary); + addParagraphs(doc.details); + addBullets('Since Tizen', doc.since); + addBullets('Privilege level', doc.privilegeLevels); + addBullets('Privileges', doc.privileges); + addBullets('Remarks', doc.remarks); + addNamedBullets('Parameters', doc.parameters); + addBullets('Returns', doc.returns); + addNamedBullets('Return values', doc.returnValues); + addNamedBullets('Exceptions', doc.exceptions); + addBullets('Preconditions', doc.preconditions); + addBullets('Postconditions', doc.postconditions); + addBullets('Notes', doc.notes); + addBullets('Warnings', doc.warnings); + addBullets('See also', doc.seeAlso); + + for (final entry in doc.extraSections.entries) { + addBullets(entry.key, entry.value); + } + + for (final codeBlock in doc.codeBlocks) { + if (codeBlock.title != null && codeBlock.title!.isNotEmpty) { + final title = codeBlock.title!.replaceFirst( + _titleTrailingColonRegExp, + '', + ); + _appendDocParagraph(output, '**$title:**'); + } + _appendCodeBlock(output, codeBlock); + } + + if (output.isNotEmpty && output.last == '///') { + output.removeLast(); + } + + return output; +} + +_StructuredDoc _parseStructuredDoc(List docLines) { + final doc = _StructuredDoc(); + void Function(String text)? appendContinuation; + final pendingParagraphTitles = []; + List? currentCodeBlock; + String? currentCodeLanguage; + + void addParagraph( + List target, + String text, { + bool appendToCurrentItem = true, + }) { + final normalized = _normalizeInlineDocText(text); + if (normalized.isEmpty) { + return; + } + target.add(normalized); + appendContinuation = (more) { + final nextText = _normalizeInlineDocText(more); + if (nextText.isEmpty) { + return; + } + if (appendToCurrentItem) { + target[target.length - 1] = _mergeDocText( + target[target.length - 1], + nextText, + ); + return; + } + target.add(nextText); + }; + } + + void addNamedItem( + List<_DocItem> target, + String? name, + String description, { + String? direction, + }) { + final normalized = _normalizeInlineDocText(description); + target.add( + _DocItem( + name: name == null || name.isEmpty ? null : name, + direction: direction == null || direction.isEmpty ? null : direction, + description: normalized, + ), + ); + appendContinuation = (more) { + target[target.length - 1].append(_normalizeInlineDocText(more)); + }; + } + + void addExtra(String label, String text) { + final normalized = _normalizeInlineDocText(text); + if (normalized.isEmpty) { + return; + } + doc.addExtra(label, normalized); + appendContinuation = (more) { + final items = doc.extraSections[label]!; + items[items.length - 1] = _mergeDocText( + items[items.length - 1], + _normalizeInlineDocText(more), + ); + }; + } + + void flushPendingParagraphTitles() { + while (pendingParagraphTitles.isNotEmpty) { + addExtra('Paragraph', pendingParagraphTitles.removeAt(0)); + } + } + + for (final rawDocLine in docLines) { + final stripped = _stripGeneratedDocLine(rawDocLine); + + if (currentCodeBlock != null) { + final trimmed = stripped.trim(); + if (trimmed == r'@endcode' || trimmed == r'\endcode') { + final title = pendingParagraphTitles.isNotEmpty + ? pendingParagraphTitles.removeLast() + : null; + doc.codeBlocks.add( + _CodeBlock( + lines: List.from(currentCodeBlock), + title: title, + language: currentCodeLanguage, + ), + ); + currentCodeBlock = null; + currentCodeLanguage = null; + appendContinuation = null; + continue; + } + currentCodeBlock.add(_normalizeCodeLine(stripped)); + continue; + } + + final expandedLines = _expandDocLine(stripped); + + for (final expandedLine in expandedLines) { + final line = expandedLine.trimRight(); + final trimmed = line.trim(); + + if (trimmed.isEmpty) { + appendContinuation = null; + continue; + } + + final codeBlockStartMatch = _codeBlockStartRegExp.firstMatch(trimmed); + if (codeBlockStartMatch != null) { + currentCodeBlock = []; + currentCodeLanguage = _normalizeCodeBlockLanguage( + codeBlockStartMatch.group(1), + ); + appendContinuation = null; + continue; + } + + final briefMatch = _briefTagRegExp.firstMatch(trimmed); + if (briefMatch != null) { + addParagraph(doc.summary, briefMatch.group(1)!); + continue; + } + + final detailsMatch = _detailsTagRegExp.firstMatch(trimmed); + if (detailsMatch != null) { + addParagraph(doc.details, detailsMatch.group(1)!); + continue; + } + + final deprecatedMatch = _deprecatedTagRegExp.firstMatch(trimmed); + if (deprecatedMatch != null) { + final text = deprecatedMatch.group(1)!.trim(); + addParagraph( + doc.deprecations, + text.isEmpty + ? '**Deprecated.**' + : '**Deprecated:** ${_normalizeConditionalInlineText(text)}', + ); + continue; + } + + final sinceTizenMatch = _sinceTizenTagRegExp.firstMatch(trimmed); + if (sinceTizenMatch != null) { + addParagraph(doc.since, _formatSinceText(sinceTizenMatch.group(1)!)); + continue; + } + + final sinceMatch = _sinceTagRegExp.firstMatch(trimmed); + if (sinceMatch != null) { + addParagraph(doc.since, sinceMatch.group(1)!); + continue; + } + + final privlevelMatch = _privlevelTagRegExp.firstMatch(trimmed); + if (privlevelMatch != null) { + addParagraph(doc.privilegeLevels, privlevelMatch.group(1)!); + continue; + } + + final privilegeMatch = _privilegeTagRegExp.firstMatch(trimmed); + if (privilegeMatch != null) { + addParagraph( + doc.privileges, + privilegeMatch.group(1)!, + appendToCurrentItem: false, + ); + continue; + } + + final remarksMatch = _remarksTagRegExp.firstMatch(trimmed); + if (remarksMatch != null) { + addParagraph( + doc.remarks, + remarksMatch.group(1)!, + appendToCurrentItem: false, + ); + continue; + } + + final paramMatch = _paramTagRegExp.firstMatch(trimmed); + if (paramMatch != null) { + addNamedItem( + doc.parameters, + paramMatch.group(2), + paramMatch.group(3) ?? '', + direction: paramMatch.group(1), + ); + continue; + } + + final returnMatch = _returnTagRegExp.firstMatch(trimmed); + if (returnMatch != null) { + addParagraph(doc.returns, returnMatch.group(1)!); + continue; + } + + final retvalMatch = _retvalTagRegExp.firstMatch(trimmed); + if (retvalMatch != null) { + final parsed = _parseNamedDocItem(retvalMatch.group(1)!); + addNamedItem(doc.returnValues, parsed.name, parsed.description); + continue; + } + + final exceptionMatch = _exceptionTagRegExp.firstMatch(trimmed); + if (exceptionMatch != null) { + final parsed = _parseNamedDocItem(exceptionMatch.group(1)!); + addNamedItem(doc.exceptions, parsed.name, parsed.description); + continue; + } + + final preMatch = _preTagRegExp.firstMatch(trimmed); + if (preMatch != null) { + addParagraph(doc.preconditions, preMatch.group(1)!); + continue; + } + + final postMatch = _postTagRegExp.firstMatch(trimmed); + if (postMatch != null) { + addParagraph(doc.postconditions, postMatch.group(1)!); + continue; + } + + final noteMatch = _noteTagRegExp.firstMatch(trimmed); + if (noteMatch != null) { + addParagraph(doc.notes, noteMatch.group(1)!); + continue; + } + + final warningMatch = _warningTagRegExp.firstMatch(trimmed); + if (warningMatch != null) { + addParagraph(doc.warnings, warningMatch.group(1)!); + continue; + } + + final seeMatch = _seeTagRegExp.firstMatch(trimmed); + if (seeMatch != null) { + addParagraph(doc.seeAlso, _normalizeSeeAlsoText(seeMatch.group(1)!)); + continue; + } + + final parMatch = _parTagRegExp.firstMatch(trimmed); + if (parMatch != null) { + final title = _normalizeInlineDocText(parMatch.group(1)!); + if (title.isNotEmpty) { + pendingParagraphTitles.add(title); + } + appendContinuation = null; + continue; + } + + final featureMatch = _featureTagRegExp.firstMatch(trimmed); + if (featureMatch != null) { + addExtra('Required feature', featureMatch.group(1)!); + continue; + } + + final platformMatch = _platformTagRegExp.firstMatch(trimmed); + if (platformMatch != null) { + final text = platformMatch.group(1)!.trim(); + addExtra('Platform', text.isEmpty ? 'Platform API.' : text); + continue; + } + + final partnerMatch = _partnerTagRegExp.firstMatch(trimmed); + if (partnerMatch != null) { + addExtra('Partner', partnerMatch.group(1)!); + continue; + } + + final internalMatch = _internalTagRegExp.firstMatch(trimmed); + if (internalMatch != null) { + addExtra('Internal', internalMatch.group(1)!); + continue; + } + + final wearableOnlyMatch = _wearableOnlyTagRegExp.firstMatch(trimmed); + if (wearableOnlyMatch != null) { + final text = wearableOnlyMatch.group(1)!.trim(); + addExtra( + 'Platform restriction', + text.isEmpty ? 'Wearable only.' : text, + ); + continue; + } + + final sectionMatch = _sectionTagRegExp.firstMatch(trimmed); + if (sectionMatch != null) { + addExtra('Section', sectionMatch.group(1)!); + continue; + } + + final groupMatch = _groupTagRegExp.firstMatch(trimmed); + if (groupMatch != null) { + addExtra('Group', groupMatch.group(1)!); + continue; + } + + final miscTagMatch = _miscTagRegExp.firstMatch(trimmed); + if (miscTagMatch != null) { + addExtra( + _uppercaseFirst(miscTagMatch.group(1)!), + miscTagMatch.group(2)!, + ); + continue; + } + + if (appendContinuation != null) { + appendContinuation!(trimmed); + continue; + } + + if (pendingParagraphTitles.isNotEmpty) { + addExtra(pendingParagraphTitles.removeAt(0), trimmed); + continue; + } + + addParagraph( + doc.summary.isEmpty && doc.details.isEmpty ? doc.summary : doc.details, + trimmed, + ); + } + } + + if (currentCodeBlock != null) { + doc.codeBlocks.add( + _CodeBlock( + lines: List.from(currentCodeBlock), + title: pendingParagraphTitles.isNotEmpty + ? pendingParagraphTitles.removeLast() + : null, + language: currentCodeLanguage, + ), + ); + } + + flushPendingParagraphTitles(); + return doc; +} + +_DocItem _parseNamedDocItem(String text) { + final normalized = _normalizeInlineDocText(text); + final match = _namedDocItemRegExp.firstMatch(normalized); + if (match == null) { + return _DocItem(description: normalized); + } + + return _DocItem( + name: match.group(1), + description: (match.group(2) ?? '').trim(), + ); +} + +void _appendDocParagraph(List output, String paragraph) { + if (paragraph.trim().isEmpty) { + return; + } + if (output.isNotEmpty) { + output.add('///'); + } + for (final line in paragraph.split('\n')) { + output.add(line.isEmpty ? '///' : '/// $line'); + } +} + +void _appendDocBullets( + List output, + String heading, + List items, +) { + final filteredItems = items.where((item) => item.trim().isNotEmpty).toList(); + if (filteredItems.isEmpty) { + return; + } + if (output.isNotEmpty) { + output.add('///'); + } + output.add('/// **$heading:**'); + for (final item in filteredItems) { + final bulletText = item.startsWith('http://') || item.startsWith('https://') + ? '<$item>' + : item; + final lines = bulletText.split('\n'); + output.add('/// - ${lines.first}'); + for (var i = 1; i < lines.length; i++) { + output.add(lines[i].isEmpty ? '///' : '/// ${lines[i]}'); + } + } +} + +void _appendDocNamedBullets( + List output, + String heading, + List<_DocItem> items, +) { + final filteredItems = + items.where((item) => item.description.trim().isNotEmpty).toList(); + if (filteredItems.isEmpty) { + return; + } + if (output.isNotEmpty) { + output.add('///'); + } + output.add('/// **$heading:**'); + for (final item in filteredItems) { + final firstLineBuffer = StringBuffer('/// - '); + String description = item.description; + if (item.name != null) { + final displayName = item.name!.startsWith('`') && item.name!.endsWith('`') + ? item.name! + : '`${item.name}`'; + firstLineBuffer.write(displayName); + if (item.direction != null && item.direction!.isNotEmpty) { + firstLineBuffer.write(' (${item.direction})'); + } + if (description.isNotEmpty) { + firstLineBuffer.write(': '); + } + } + + if (description.isNotEmpty) { + final lines = description.split('\n'); + firstLineBuffer.write(lines.first); + output.add(firstLineBuffer.toString()); + for (var i = 1; i < lines.length; i++) { + output.add(lines[i].isEmpty ? '///' : '/// ${lines[i]}'); + } + } else { + output.add(firstLineBuffer.toString()); + } + } +} + +void _appendCodeBlock(List output, _CodeBlock codeBlock) { + if (codeBlock.lines.isEmpty) { + return; + } + if (output.isNotEmpty) { + output.add('///'); + } + output.add( + codeBlock.language == null ? '/// ```' : '/// ```${codeBlock.language}', + ); + for (final line in codeBlock.lines) { + output.add(line.isEmpty ? '///' : '/// $line'); + } + output.add('/// ```'); +} + +String _stripGeneratedDocLine(String line) { + final stripped = line.startsWith('///') ? line.substring(3) : line; + return stripped.startsWith(' ') ? stripped.substring(1) : stripped; +} + +List _expandDocLine(String line) { + final expanded = line + .replaceAll(r'\n', '\n') + .replaceAll(_encodedNewlineMarkerRegExp, '\n'); + final parts = expanded.split('\n'); + while (parts.isNotEmpty && parts.last.isEmpty) { + parts.removeLast(); + } + return parts; +} + +String _formatSinceText(String text) { + final trimmed = text.trim(); + if (!trimmed.contains('@if')) { + return _normalizeInlineDocText(trimmed); + } + + final items = []; + for (final match in _conditionalSinceBranchRegExp.allMatches(trimmed)) { + final platform = _formatPlatformLabel(match.group(1)!); + final version = _normalizeInlineDocText(match.group(2)!); + items.add('$platform $version'); + } + + final elseMatch = _conditionalElseRegExp.firstMatch(trimmed); + if (elseMatch != null) { + items.add('Otherwise ${_normalizeInlineDocText(elseMatch.group(1)!)}'); + } + + if (items.isNotEmpty) { + return items.join('; '); + } + + return _normalizeInlineDocText( + trimmed + .replaceAll('@if', '') + .replaceAll('@elseif', '') + .replaceAll('@else', '') + .replaceAll('@endif', ''), + ); +} + +String _formatPlatformLabel(String value) { + return value + .split('_') + .where((part) => part.isNotEmpty) + .map((part) => '${part[0]}${part.substring(1).toLowerCase()}') + .join(' '); +} + +String _normalizeSeeAlsoText(String text) { + final normalized = _normalizeInlineDocText(text); + if (normalized.isEmpty) { + return normalized; + } + if (normalized.startsWith('http://') || normalized.startsWith('https://')) { + return '<$normalized>'; + } + if (normalized.startsWith('`') && normalized.endsWith('`')) { + return normalized; + } + if (_simpleReferenceRegExp.hasMatch(normalized)) { + return '`$normalized`'; + } + return normalized; +} + +String _normalizeConditionalInlineText(String text) { + final trimmed = text.trim(); + if (!trimmed.contains('@if')) { + return _normalizeInlineDocText(trimmed); + } + + final conditionalMatch = _conditionalInlineBlockRegExp.firstMatch(trimmed); + if (conditionalMatch == null) { + return _normalizeInlineDocText(trimmed); + } + + final replacement = _formatSinceText(conditionalMatch.group(0)!); + final replaced = trimmed.replaceRange( + conditionalMatch.start, + conditionalMatch.end, + replacement, + ); + return _normalizeInlineDocText(replaced); +} + +String _normalizeInlineDocText(String text) { + var normalized = text.trim(); + if (normalized.isEmpty) { + return normalized; + } + + normalized = normalized.replaceAll(r'\n', ' '); + normalized = normalized.replaceAll(_encodedNewlineMarkerRegExp, ' '); + normalized = normalized.replaceAll('%http://', 'http://'); + normalized = normalized.replaceAll('%https://', 'https://'); + + normalized = normalized.replaceAllMapped( + _inlineRefRegExp, + (match) => '`${match.group(1)}`', + ); + normalized = normalized.replaceAllMapped(_inlineFormattingTagRegExp, (match) { + final marker = match.group(1)!; + final rawToken = match.group(2)!; + final tokenMatch = _trailingPunctuationRegExp.firstMatch(rawToken)!; + final token = tokenMatch.group(1)!; + final punctuation = tokenMatch.group(2)!; + final formatted = switch (marker) { + 'a' || 'c' => '`$token`', + 'b' => '**$token**', + 'e' => '*$token*', + _ => token, + }; + return '$formatted$punctuation'; + }); + normalized = normalized.replaceAllMapped( + _hashReferenceRegExp, + (match) => '`${match.group(1)}`', + ); + // Normalize existing bracket escaping (idempotency) + normalized = normalized.replaceAllMapped( + RegExp(r'`+\[([^\[\]]+)\]`+'), + (match) => '`[${match.group(1)}]`', + ); + normalized = normalized.replaceAllMapped( + _squareBracketLiteralRegExp, + (match) => '`[${match.group(1)}]`', + ); + normalized = normalized.replaceAll(_whitespaceRegExp, ' '); + return normalized.trim(); +} + +String _normalizeCodeLine(String line) { + return line.trimRight(); +} + +String? _normalizeCodeBlockLanguage(String? language) { + if (language == null) { + return null; + } + + final normalized = language.trim(); + if (normalized.isEmpty) { + return null; + } + + return normalized.startsWith('.') ? normalized.substring(1) : normalized; +} + +String _mergeDocText(String current, String next) { + if (current.isEmpty) { + return next; + } + if (next.isEmpty) { + return current; + } + final nextTrimmed = next.trimLeft(); + if (nextTrimmed.startsWith('- ') || + nextTrimmed.startsWith('* ') || + nextTrimmed.startsWith('```') || + nextTrimmed.startsWith('< ') || + nextTrimmed.startsWith('Privilege :') || + nextTrimmed.startsWith('{@') || + (nextTrimmed.startsWith('**') && nextTrimmed.contains(':**')) || + _listRegExp.hasMatch(nextTrimmed)) { + return '$current\n$next'; + } + if (current.endsWith('\n') || current.trimRight().endsWith('<')) { + return '$current\n$next'; + } + return '$current $next'; +} + +String _uppercaseFirst(String value) { + if (value.isEmpty) { + return value; + } + return '${value[0].toUpperCase()}${value.substring(1)}'; +} diff --git a/scripts/generate_tizen.py b/scripts/generate_tizen.py index 297d12f8..a9fa1ea0 100644 --- a/scripts/generate_tizen.py +++ b/scripts/generate_tizen.py @@ -97,7 +97,9 @@ def main(): # 3. Generate tizen.dart lines = [] - lines.append("library tizen_interop;") + version_underscored = version.replace('.', '_') + lines.append(f"/// Tizen Interop for Tizen {version}.") + lines.append(f"library tizen_interop_{version_underscored};") lines.append("") lines.append("import 'dart:ffi';") lines.append("")