Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ doc/api/
.failed_tracker
custom_lint.log
logs/analyzer.txt
.mcp_logs/
18 changes: 17 additions & 1 deletion lib/src/document/document.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,28 @@ class Document {
for (final line in _lines) {
/// wait until we see a line that is after the parent.
if (line.lineNo > parent.lineNo) {
final isCommentOrBlank = line.lineType == LineType.comment ||
line.lineType == LineType.blank;

/// If the ident decreases then we have passed all
/// of the parent's children.
if (line.indent <= parent.indent) {
if (!isCommentOrBlank && line.indent <= parent.indent) {
break;
}

// Comments/blanks that are not indented as children don't belong to
// this section, but they should not terminate child scanning.
if (isCommentOrBlank && line.indent <= parent.indent) {
continue;
}

// Descendant traversal is used to compose section line ownership.
// Comments/blanks are owned via the Comments model, so including them
// here can cause a line to belong to multiple sections.
if (descendants && isCommentOrBlank) {
continue;
}

/// filter out children that don't match the key [type]
if (type != null && line.lineType != type) {
continue;
Expand Down
2 changes: 2 additions & 0 deletions lib/src/pubspec/internal_parts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ part 'executable.dart';
part 'executable_builder.dart';
part 'executables.dart';
part 'homepage.dart';
part 'screenshots.dart';
part 'issue_tracker.dart';
part 'name.dart';
part 'platform_support.dart';
part 'platforms.dart';
part 'publish_to.dart';
part 'pubspec.dart';
part 'repository.dart';
part 'string_list.dart';
part 'version.dart';
part 'version_constraint.dart';
part 'version_constraint_builder.dart';
58 changes: 46 additions & 12 deletions lib/src/pubspec/pubspec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ class PubSpec {
/// Url of the documentation for the package.
late final Documentation documentation;

/// List of URLs where users can support the package maintainers.
late final StringListSection funding;

/// List of false-positive paths for accidental secret scanning.
late final StringListSection falseSecrets;

/// List of package topics.
late final StringListSection topics;

/// List of advisory IDs that are intentionally ignored.
late final StringListSection ignoredAdvisories;

/// Screenshots shown on pub.dev.
late final Screenshots screenshots;

/// List of dependencies for the package.
late final Dependencies dependencies;

Expand Down Expand Up @@ -88,15 +103,16 @@ class PubSpec {
repository = Repository.missing(document);
issueTracker = IssueTracker._missing(document);
documentation = Documentation._missing(document);
funding = StringListSection._missing(this, 'funding');
falseSecrets = StringListSection._missing(this, 'false_secrets');
topics = StringListSection._missing(this, 'topics');
ignoredAdvisories = StringListSection._missing(this, 'ignored_advisories');
screenshots = Screenshots._missing(this);
dependencies = Dependencies._missing(this, 'dependencies');
devDependencies = Dependencies._missing(this, 'dev_dependencies');
dependencyOverrides = Dependencies._missing(this, 'dependency_overrides');
platforms = Platforms._missing(this);
executables = Executables._missing(this);
// funding = SectionImpl.missing(document, 'funding');
// falseSecrets = SectionImpl.missing(document, 'false_secrets');
// screenshots = SectionImpl.missing(document, 'screenshots');
// topics = SectionImpl.missing(document, 'topics');
}

/// Loads the content of a pubspec.yaml from [content].
Expand All @@ -116,21 +132,38 @@ class PubSpec {
repository = Repository._fromDocument(document);
issueTracker = IssueTracker._fromDocument(document);
documentation = Documentation._fromDocument(document);
funding = _initStringList('funding');
falseSecrets = _initStringList('false_secrets');
topics = _initStringList('topics');
ignoredAdvisories = _initStringList('ignored_advisories');
screenshots = _initScreenshots();

dependencies = _initDependencies('dependencies');
devDependencies = _initDependencies('dev_dependencies');
dependencyOverrides = _initDependencies('dependency_overrides');
platforms = _initPlatforms();

executables = _initExecutables();
// funding = document.findSectionForKey('funding');
// falseSecrets = document.findSectionForKey('falseSecrets');
// screenshots = document.findSectionForKey('screenshots');
// topics = document.findSectionForKey('topics');

_validate();
}

StringListSection _initStringList(String key) {
final line = document.findTopLevelKey(key);
if (line.missing) {
return StringListSection._missing(this, key);
}
return StringListSection._fromLine(this, line);
}

Screenshots _initScreenshots() {
final line = document.findTopLevelKey(Screenshots.keyName);
if (line.missing) {
return Screenshots._missing(this);
}
return Screenshots._fromLine(this, line);
}

/// Loads the pubspec.yaml file from the given [directory] or
/// the current work directory if [directory] is not passed.
///
Expand Down Expand Up @@ -284,15 +317,16 @@ class PubSpec {
..render(repository)
..render(issueTracker)
..render(documentation)
..render(funding)
..render(falseSecrets)
..render(topics)
..render(ignoredAdvisories)
..render(screenshots)
..render(dependencies._section)
..render(devDependencies._section)
..render(dependencyOverrides._section)
..render(executables)
..render(platforms._section)
// ..render(funding)
// ..render(falseSecrets)
// ..render(screenshots)
// ..render(topics)
..renderMissing()
..write(writer);
}
Expand Down
181 changes: 181 additions & 0 deletions lib/src/pubspec/screenshots.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
part of 'internal_parts.dart';

class Screenshots implements Section {
static const keyName = 'screenshots';

SectionImpl _section;
// We pass pubpsec to every element for consistency
// ignore: unused_field
final PubSpec _pubspec;
final _screenshots = <Screenshot>[];

Screenshots._missing(this._pubspec)
: _section = SectionImpl.missing(_pubspec.document, keyName);

Screenshots._fromLine(this._pubspec, LineImpl line)
: _section = SectionImpl.fromLine(line) {
for (final item in line.childrenOf(type: LineType.indexed)) {
_screenshots.add(Screenshot._fromIndexedLine(item));
}
}

List<Screenshot> get list => List.unmodifiable(_screenshots);

int get length => _screenshots.length;

Screenshots add({required String description, required String path}) {
_ensureSectionExists();
var lineBefore = _section.headerLine;
if (_screenshots.isNotEmpty) {
lineBefore = _screenshots.last._lines.last;
}

final descriptionLine = LineImpl.forInsertion(_section.document,
'${_section.headerLine.childIndent}- description: $description');
lineBefore = _section.document.insertAfter(descriptionLine, lineBefore);
final pathLine = LineImpl.forInsertion(
_section.document, '${descriptionLine.childIndent}path: $path');
lineBefore = _section.document.insertAfter(pathLine, lineBefore);

_screenshots.add(Screenshot._fromAttachedLines(descriptionLine, pathLine));

return this;
}

void removeAt(int index) {
if (index < 0 || index >= _screenshots.length) {
throw RangeError.range(index, 0, _screenshots.length - 1);
}
final screenshot = _screenshots.removeAt(index);
for (final line in screenshot._lines) {
_section._removeChild(line);
}
}

void removeAll() {
for (final screenshot in _screenshots.reversed) {
for (final line in screenshot._lines.reversed) {
_section._removeChild(line);
}
}
_screenshots.clear();
}

void _ensureSectionExists() {
if (_section.missing) {
final headerLine = _section.document.append(LineDetached('$keyName:'));
_section = SectionImpl.fromLine(headerLine);
}
}

@override
Comments get comments => _section.comments;

@override
List<Line> get lines => _section.lines;

@override
bool get missing => _section.missing;
}

class Screenshot {
String _description;
String _path;
final List<LineImpl> _lines;

Screenshot._(this._description, this._path, this._lines);

factory Screenshot._fromAttachedLines(
LineImpl descriptionLine, LineImpl pathLine) =>
Screenshot._(_readValue(descriptionLine, requiredKey: 'description'),
pathLine.value, [descriptionLine, pathLine]);

factory Screenshot._fromIndexedLine(LineImpl indexedLine) {
String? description;
String? path;
final lines = _collectEntryLines(indexedLine);

final inline = _extractInlineKeyValue(indexedLine);
if (inline != null) {
switch (inline.key) {
case 'description':
description = inline.value;
case 'path':
path = inline.value;
}
}

for (final child in indexedLine.childrenOf(type: LineType.key)) {
switch (child.key) {
case 'description':
description = child.value;
case 'path':
path = child.value;
}
}

if (description == null || path == null) {
throw PubSpecException(indexedLine,
'Each screenshot entry must contain both description and path.');
}

return Screenshot._(description, path, lines);
}

String get description => _description;

String get path => _path;

static KeyValue? _extractInlineKeyValue(LineImpl indexedLine) {
final trimmed = indexedLine.text.trimLeft();
if (!trimmed.startsWith('-')) {
return null;
}
final inlineValue = trimmed.substring(1).trimLeft();
if (!inlineValue.contains(':')) {
return null;
}
return KeyValue.fromText(inlineValue);
}

static String _readValue(LineImpl line, {required String requiredKey}) {
final trimmed = line.text.trimLeft();
if (!trimmed.startsWith('-')) {
throw PubSpecException(
line, 'Invalid screenshot entry, expected "-" prefix.');
}
final keyValue = KeyValue.fromText(trimmed.substring(1).trimLeft());
if (keyValue.key != requiredKey) {
throw PubSpecException(
line, 'Invalid screenshot entry, expected key: $requiredKey.');
}
return keyValue.value;
}

static List<LineImpl> _collectEntryLines(LineImpl indexedLine) {
final lines = <LineImpl>[indexedLine];
final document = indexedLine._document;

for (final line in document._lines) {
if (line.lineNo <= indexedLine.lineNo) {
continue;
}

final isCommentOrBlank =
line.lineType == LineType.comment || line.lineType == LineType.blank;

if (!isCommentOrBlank && line.indent <= indexedLine.indent) {
break;
}

// Misindented comments/blanks should not be treated as children.
if (isCommentOrBlank && line.indent <= indexedLine.indent) {
continue;
}

lines.add(line);
}

return lines;
}
}
Loading